Основная прелесть использования ПЛИС, на мой взгляд, состоит в том, что разработка аппаратуры превращается в программирование со всеми его свойствами: написание и отладка кода как текста на специализированных языках описания аппаратуры (HDL); код распространяется в виде параметризованных модулей (IP-блоков), что позволяет его легко переиспользовать в других проектах; распределенная разработка обширным коллективом разработчиков с системой контроля версий, такой же, как у программистов (Git); и, как и в программировании, ничтожно низкая стоимость ошибки.

Последнее очень важно, так как если при разработке устройства классическим методом разработчик несет вполне существенные затраты на сборку и производство изделия, и любая схемотехническая ошибка или ошибка трассировки печатной платы — это всегда выход на очередную итерацию и попадание на деньги, то при работе с ПЛИС ошибки ничтожны по своей стоимости и легко устранимы. И даже если в серийном изделии обнаруживается ошибка, то её во многих случаях можно устранить очередным апгрейдом прошивки «в поле» без замены изделия. Короче, с приходом ПЛИС разработка цифровой аппаратуры все больше и больше выглядит как программирование, а это, помимо всего прочего, существенно понижает порог вхождения в тему, и все больше программистов становятся разработчиками «железа». А новые люди, в свою очередь, приносят с собой в индустрию новые подходы и принципы.

В этой статье я хочу поделиться своим небольшим опытом «программирования» микросхем ПЛИС и тем, как я постепенно погружался в тему ПЛИСоводства. Изначально я собирался написать небольшую заметку про открытый тулчейн для синтеза Yosys. Потом — про язык SpinalHDL и синтезируемое микропроцессорное ядро VexRiscv, на нём написанное. Потом — про замену микроконтроллеров микросхемами ПЛИС на примере моей отладочной платы «Карно». Но в процессе я погрузился в историю появления Hardware Description Languages (HDL), и когда я начал писать, Остапа, как это часто бывает, понесло... В общем, получилось то, что получилось.

А еще эту статью можно рассматривать как глубокое погружение в то, что происходит вот на этом новогоднем видео.

СОДЕРЖАНИЕ

1. Краткий экскурс в историю

2. Появления программируемых логических устройств

3. Производители микросхем ПЛИС

4. Синтез цифровых схем для ПЛИС

5. Yosys Open SYnthesis Suite

6. Как установить утилиты тулчейна Yosys

7. Пример использования открытого тулчейна Yosys

7.1. Установка и настройка «basics-graphics-music»

7.2 Лабораторная работа «01_and_or_not_xor_de_morgan»

7.3 Загрузка битстрима в микросхему ПЛИС

8. Типовой Makefile для синтеза с помощью Yosys

9. Файл описания внешних сигналов и ограничений (LPF, PCF, CST)

10. Особенности синтаксиса Yosys

10.1 Макро YOSYS

10.2 Многомерные массивы сигналов

10.3 Функция возведения в степень ($POW) и оператор **

10.4 Функция не может иметь доступ к сигналу, описанному за её пределами

10.5 Сигналы нулевой или отрицательной размерности

10.6 Сложные битовые манипуляции иногда могут выдавать ошибку о loopback-ах

10.7 Глобализация сигналов

11. Анализируем сообщения от тулов

11.1 Сообщения от утилиты yosys

11.2 Сообщения от утилиты nextpnr

12. Синтезируемая ЭВМ

12.1 Синтезируемое вычислительное ядро VexRiscv

12.2 Синтезируемые системы-на-кристалле на базе VexRiscv

13. Язык описания аппаратуры SpinalHDL

13.1 Базовые конструкции языка SpinalHDL

13.2 FSM, потоки, конвейеры, тактовые домены

13.3 Установка SpinalHDL

13.4 Разбор шаблона SpinalTemplateSbt

13.5 Подготовка Makefile-а для синтеза из SpinalHDL в битстрим

13.6 Анализируем вывод сообщений SpinalHDL

13.7 Симуляция и верификация в SpinalHDL

14. Синтез вычислительного ядра VexRiscv

14.1 Знакомство с репозиторием VexRiscv

14.2 Установка компилятора для архитектуры RISC-V

14.3 Подготавливаем Makefile, LPF и toplevel.v

14.4 Добавляем точку входа для генерации СнК Murax для платы «Карно»

14.5 Модифицируем код программы «hello_world»

14.6 Сборка СнК Murax и ядра VexRiscv

14.7 Запускаем СнК Murax на ПЛИС и тестируем «hello_world»

15. Устройство синтезируемого СнК Murax и ядра VexRiscv

15.1 Структура СнК Murax

15.2 Структура вычислительного ядра VexRiscv

15.3 Плагины вычислительного ядра VexRiscv

16. Эксперименты с ядром VexRiscv и СнК Murax

16.1 Оптимизация на примере плагина RegFilePlugin

16.2 Увеличиваем тактовую частоту ядра используя встроенный PLL

16.3 Увеличиваем производительность инструкций сдвига

17. Добавляем свои аппаратные блоки (IP-блоки)

17.1 Микросекундный машинный таймер MTIME

17.2 Подключаем микросхему SRAM

17.2.1 Разрабатываем контроллер SRAM

17.2.2 Тестируем память и контроллер SRAM

17.2.3 Используем SRAM с функцией malloc

17.2.4 Добавляем блоки вычислителей Div и Mul

17.2.5 Задействуем функцию printf

17.3 Подключаем контроллер прерываний PLIC

17.3.1 Разрабатываем простейший контроллер прерываний MicroPLIC

17.3.2 Задействуем контроллер прерываний MicroPLIC из Си программы

17.4 Подключаем контроллер FastEthernet (MAC)

17.4.1 Как работает Ethernet

17.4.2 Подключаем компонент MacEth

17.4.3 Разрабатываем драйвер для компонента MacEth

17.4.4 Отправляем запрос в DHCP сервер

18. Потактовая симуляция СнК Murax

19. Более сложный пример: RiscvWithHUB12ForKarnix

20. Эпилог

Ссылки на репозитории

1. Краткий экскурс в историю

История микросхем началась в 1960 году, когда группа Джея Ласта из Fairchild Semiсonductors продемонстрировала первый работающий полупроводниковый прибор, изготовленный по планарной технологии. Прибор получил название «Интегральная Схема» (или IC от «Integrated Circuit»), а в русскоязычной литературе часто используют просто слово «микросхема», независимо от степени её сложности. История создания микросхемы весьма запутанная, так как этому событию предшествовала серия открытий и изобретений, выполненных различными людьми из разных компаний и стран, поэтому вслед за изобретением микросхемы последовали патентные войны — каждый пытался откусить от пирога кусок поболее. Первая микросхема в СССР была создана в 1961 году в Таганрогском радиотехническом институте. В течении последующих 10 лет мировой радиоэлектронной промышленностью было предложено и испытано множество способов производства микросхем, почти все они полагались на планарный метод изготовления транзисторов на кремниевой, германиевой или сапфировой пластине с помощью процесса фотолитографии — итеративным процессом последовательного нанесения фоторезиста, засветки, проявления, травления, промывки, ионной имплантации и металлизации, а схема, которая образовывалась таким образом, задавалась набором фотомасок.

Примерно до средины 1980-х годов, а это почти 25 лет, все аналоговые и цифровые микросхемы, в том числе микросхемы памяти и микропроцессоров, разрабатывались исключительно путем черчения схем, сначала на бумаге, позже — с использованием САПР, которые по тем временам были весьма примитивны. Но в обоих случаях рисовалась схема электрическая принципиальная, в которой прослеживался каждый проводник. Когда схема была готова, разработчики переходили к её трассировке — формированию расположения отдельных электронных компонентов (транзисторов, резисторов и конденсаторов) на поверхности кристалла, по трассировке формировали набор фотомасок. В этом им часто помогала специальная клейкая лента-скотч Рубилит, которую тонкими полосками наклеивали на прозрачную майларовую пленку (или наоборот — удаляли лишнее), таким образом формируя узор будущей микросхемы в увеличенном формате. Этот узор фотографическим методом и с уменьшением в 100 раз переносился на фотомаску, которая далее использовалась в планарном фотолитографическом производстве.

Рис. 1. Rubylith operators, 1970, Courtesy of the Intel Corporation.
Рис. 1. Rubylith operators, 1970, Courtesy of the Intel Corporation.

Микросхемы очень быстро росли в степени интеграции — увеличении числа транзисторов и уменьшении площади на кристалле кремния, что в геометрической прогрессии тянуло за собой увеличение сложности изготовления фотомасок и их стоимости. Очевидно, что такой метод разработки микросхем не мог обойтись без ошибок и для достижения рабочего образца приходилось выполнить порой до десятка итераций, что также не могло не сказываться на стоимости продукции. Изготовление рабочего набора фотомасок занимало значительную часть финансовых и временных затрат на производство микросхемы. Для того, чтобы минимизировать затраты и сократить время, разработчикам приходилось вручную проверять и перепроверять свои дизайны на всех этапах. В какой-то момент схемотехника достигла такой сложности, что проверить её работоспособность вручную стало практически невозможно и логичным решением этой проблемы стало внедрение автоматизированных процессов верификации и симуляции дизайнов. А это означало, что весь дизайн, от схемы до фотомасок, должен был быть теперь представлен в цифровом формате, т. е. в виде данных в файлах: схемы в виде связанного графа элементов (нетлиста), а фотомаски — в виде последовательности команд на языке машин фотоплоттеров (Gerber). Так, для представления схемы в виде соединения отдельных её элементов в электронном виде появились языки описания аппаратуры — HDL от «Hardware Description Language», а процесс разработки стали назвать «VLSI design» от «Very Large Scale Integration» (в русскоязычной литературе — СБИС от «Сверх Большая Интегральная Схема»).

Языки описания аппаратуры (HDL) позволяют представить прежде всего цифровую схему (хотя конечно же есть языки для описания аналоговых схем) в виде текста, подобно языкам программирования, описывают структуру схемы, потоки данных и её поведение с течением времени. История появления HDL насчитывает множество способов описания схем, но способ представления схем на уровне регистровых передач (RTL от «Register Transfer Level») впервые предложенный Гордоном Беллом в 1971 году оказался наиболее удобен для верификации сложных цифровых схем. На основе RTL был создан языке ISP (ISPL и ISPS) на котором была создана модель ЭВМ DEC PDP-8. С тех пор RTL метод используется во всех современных HDL языках для разработки и верификации VLSI дизайнов.

Рис 2а. Уровни абстракции в цифровой электронике.
Рис 2а. Уровни абстракции в цифровой электронике.

Что бы понять, что такое RTL, необходимо представить цифровую схему в виде отдельных регистров (D-триггеров), управляемых общим тактовым сигналом, а входы и выходы этих регистров обеспечить комбинационной логикой и связать между собой в заданную схему. Таким образом, все цифровые схемы в терминах RTL являются синхронными — данные в них перетекают из одного регистра в другой по переднему фронту тактового сигнала (иногда по спаду). Данные, перетекая между регистрами, претерпевают изменения (обработку) комбинационными схемами, которые работают асинхронно, а задержка распространения сигнала в них не учитывается на уровне RTL, т. е. условно полается, что комбинационные схемы срабатывают мгновенно и 100-процентно (что, разумеется, не так, но об этом отдельно). Ниже на рисунке 2 приведена простейшая синхронная цифровая схема, состоящая из одного регистра (триггера) и одного инвертора. Схема представляет собой делитель тактовой частоты на 2.

Рис. 2б. Пример элементарной цифровой синхронной схемы (RTL уровень).
Рис. 2б. Пример элементарной цифровой синхронной схемы (RTL уровень).

Приведенная выше схема может быть описана текстом на языке описания аппаратуры следующим образом:

if rising_edge(clk) then
	Q <= ~D;
end if;

Это означает, что выходной сигнал Q примет значение инверсное (противоположное) входному сигналу D в следующем такте (сигнал сброса не показан для упрощения понимания).

Данный текст на HDL может быть не только просимулирован, но и с помощью специальных программных средств т. н. «кремниевых компиляторов» может быть автоматически преобразован сначала в связанный граф примитивов (отдельных логических элементов и транзисторов), а затем в их геометрическое представление на кристалле кремния и в конечном счете может быть получен набор фотомасок. В современном производстве фотомаски для изготовления микросхем представляются в электронном формате GDSII, а формат Gerber занял нишу в производстве печатных плат. Процесс преобразования текста HDL в файлы фотомасок раньше было принято называть «аппаратной компиляцией», но сейчас больше используется термин «синтез цифровых схем» (logic synthesis), так как конечным результатом может быть не только геометрическая форма (фотомаска), но некий поток двоичных данных, определяющий конфигурацию микросхемы ПЛИС для реализации заданной цифровой схемы внутри неё.

История гласит, что легендарный микропроцессор Intel 80386 является первым микропроцессором компании Intel, который был полностью разработан с помощью цифрового инструментария. Для него была построена полная RTL модель, дизайн итеративно дорабатывался и симулировался потактово до тех пор, пока не было достигнуто совпадение всех симулируемых сигналов с проектными значениями, или, как принято говорить, с «эталонной моделью» (golden reference model). Вот что пишет известный блоггер Кен Ширрифф в свой статье «Examining the silicon dies of the Intel 386 processor» про историю разработки i386:

The design process of the 386 is interesting because it illustrates Intel's migration to automated design systems and heavier use of simulation. At the time, Intel was behind the industry in its use of tools so the leaders of the 386 realized that more automation would be necessary to build a complex chip like the 386 on schedule. By making a large investment in automated tools, the 386 team completed the design ahead of schedule. Along with proprietary CAD tools, the team made heavy use of standard Unix tools such as sed, awk, grep, and make to manage the various design databases.

И далее:

The high-level design of the chip (register-level RTL) was created and refined until clock-by-clock and phase-by-phase timing were represented. The RTL was programmed in MAINSAIL, a portable Algol-like language based on SAIL (Stanford Artificial Intelligence Language). Intel used a proprietary simulator called Microsim to simulate the RTL, stating that full-chip RTL simulation was "the single most important simulation model of the 80386".

The next step was to convert this high-level design into a detailed logic design, specifying the gates and other circuitry using Eden, a proprietary schematics-capture system. Simulating the logic design required a dedicated IBM 3083 mainframe that compared it against the RTL simulations. Next, the circuit design phase created the transistor-level design. The chip layout was performed on Applicon and Eden graphics systems...

Рис.3.Фотография кристалла микропроцессора 80386 с обозначением функциональных областей. 1985г. Заимствованно из блога Кена Ширриффа, сайт http://www.righto.com/
Рис.3.Фотография кристалла микропроцессора 80386 с обозначением функциональных областей. 1985г. Заимствованно из блога Кена Ширриффа, сайт http://www.righto.com/

В современной практике широкое использование получили два языка описания аппаратуры: VHDL и Verilog. Verilog был предложен частной компанией Gateway Design Automation и входил в состав коммерческой системы разработки микросхем. Взамен проприетарному Verilog-у в недрах минобороны США был разработан VHDL и предложен как открытый стандарт. Изначально оба языка предназначались для документирования и симуляции цифровых схем, но к 1985 году стало понятно, что разрабатывать схемы «в тексте» гораздо легче, чем рисовать их, пусть даже на экране монитора. Такие схемы легче анализировать и модифицировать, легче систематизировать и каталогизировать, над одним и тем же куском кода (схемы) может работать несколько человек, но самое главное — код схемы можно переиспользовать от дизайна к дизайну, легко изменяя небольшую его часть. Код схемы даже может быть параметризован, выделен в отдельные блоки, упакован и продан. Появления HDL привело к созданию целой индустрии купли-продажи IP блоков (здесь IP от «Intellectual Property» — интеллектуальная собственность).

В 1990 году компания Cadence приобрела компанию Gateway Design Automation и сделала Verilog открытым, вскоре после чего оба языка были приняты IEEE как открытые стандарты. За почти 40-летнюю историю существования языки развивались, в них вносились серьезные изменения, добавлялись и расширялись стандартные библиотеки (VHDL), вводились и принимались новые стандарты. VHDL традиционно имел широкое хождение в авиакосмической и военной отрасли (как в США, так и в России, кстати), в то время как Verilog больше популярен в академической среде и в небольших коммерческих компаниях. На начало 2020 годов проявилась явная тенденция к превалированию языка Verilog и к спаду популярности VHDL. С чем это связано — мне сказать трудно.

В 2002 году от языка Verilog отпочковался SystemVerilog который в 2005 году так же был принял стандартом IEEE 1800-2005, а в 2009 году язык Verilog вошел как подмножество в язык SystemVerilog и был принят как стандарт IEEE 1800-2009 SystemVerilog. Отличие Verilog от SystemVerilog примерно такое же, как отличие языков C и C++. SystemVerilog поддерживает классы и объекты, а также сложные структуры сигналов. В современной практике, когда говорят Verilog, подразумевают именно SystemVerilog, либо явно подчеркивают его старую версию стандарта.

Ниже я приведу в качестве примера схему двоичного 4-х битового полного сумматора описанную на двух языках. На всякий случай напомню, что полным сумматором называется такой сумматор, который выполняет сложение двух входных двоичных чисел заданной разрядности с учетом входного сигнала переноса и формированием на выходе как суммы, так и выходного сигнала переноса, что позволяет соединять полные сумматоры в каскады и получать сумматоры большей битности.

Рис. 4. Схема 4-х битового полного сумматора на языке VHDL.
Рис. 4. Схема 4-х битового полного сумматора на языке VHDL.
Рис. 5. Схема 4-х битового полного сумматора на языке Verilog (IEEE 1364-2001).
Рис. 5. Схема 4-х битового полного сумматора на языке Verilog (IEEE 1364-2001).
Рис. 6. Схема 4-х битового полного сумматора синтезированная из выше приведенного кода на языке Verilog.
Рис. 6. Схема 4-х битового полного сумматора синтезированная из выше приведенного кода на языке Verilog.

Из приведенного выше примера видно, что VHDL гораздо более многословен, а его синтаксис не всегда понятен и очевиден. В то же время, синтаксис языка Verilog в какой-то степени похож на синтаксис языка Си. Возможно это является одной из причин падения популярности языка VHDL, так как за последние годы в тему разработки цифровых схем вливается всё больше программистов.

2. Появления программируемых логических устройств

Идея сделать логические схемы программно реконфигурируемыми пришла вместе с появлением микросхем памяти (ОЗУ, ПЗУ) в середине 1960х, так как любую микросхему памяти можно рассматривать как логическое устройство, преобразующее набор входящих сигналов, поступающих на линии адреса, в выходящие — снимаемые с линий данных. В этом смысле любая микросхема памяти является аппаратной таблицей преобразования («lookup table», «таблица истинности») и может быть использована для построения перепрограммируемых дешифраторов. И действительно, такое её применение часто использовалось в процессорах больших ЭВМ древности для построения дешифраторов команд, да и по сей день в современных микропроцессорах дешифратор команд часто строится на перепрограммируемой ПЗУ. Построение дешифратора команд на основе перепрограммируемой ПЗУ является поистине великим изобретением, так как позволяет относительно легко исправлять ошибки в системе команд (но не всегда, привет Intel-у) либо вести отладку процессора загружая в память дешифратора специально подготовленные тестовые последовательности.

Использование перепрограммируемых ПЗУ для построения логических схем, разумеется, имеет свои ограничение. Пожалуй, самое очевидное из них — это гигантское число ячеек памяти, необходимое для построения дешифраторов с большим числом входов и выходов. Вспомним, что в 60-х и 70-х годах прошлого века компьютеры были большие, а память у них была крохотной и жутко дорогой. Поэтому в начале 70-х годов сразу несколько компаний, среди которых были такие известные и ныне гиганты полупроводниковой индустрии, как Texas Instruments, General Electric и National Semiconductor, представили на рынок свои варианты микросхем, содержащих массивы логических элементов, соединения между которыми можно было в некоторой степени конфигурировать программно — путем одноразового пережигания перемычек, т. е. использовался тот же самый принцип, что и при программировании ПЗУ. А в некоторых изделиях «прошивку» можно было даже стирать ультрафиолетом и перепрограммировать перемычки повторно. Такие изделия получили название «Programmable Logic Array» — PLA или PAL.

В микросхемах PAL использовалась регулярная структура (рис. 7) из логических элементов «И», обычно рисуемых на схемах горизонтально, выходы которых соединялись логическими элементами «ИЛИ», рисуемых вертикально, таким образом образуя «сумму произведений» (sum of product), что соответствует дизъюнктивной нормальной форме (ДНФ), к которой, согласно булевой алгебре, может быть сведена любая булева формула. Данные структуры в микросхемах PAL, многократно повторяясь, образовывали массив, используя который можно было реализовать любую булеву функцию.

Рис. 7. Упрощенная схема PAL — программируемого логического массива. Программируемые элементы в этой схеме это «фьюзы» - пережигаемые и восстанавливаемые предохранители.
Рис. 7. Упрощенная схема PAL — программируемого логического массива. Программируемые элементы в этой схеме это «фьюзы» - пережигаемые и восстанавливаемые предохранители.

Для программирования микросхем PLA/PAL в 1983 года компанией Monolithic Memories, Inc был разработан специальный язык описания программируемой логики — PALASM, который позволял преобразовать булевы функции и таблицы переходов состояний в таблицу пережигаемых «фьюзов». Компания MMI производила свой вариант микросхемы программируемой логики PAL16R4, появление такого программного инструмента сделало эту микросхему очень популярной, а сам язык PALASM превратился в стандарт «де факто», который использовали подавляющее большинство разработчиков, имеющих дело с PAL.

Рис. 8. Пример реализации 4-х битного счетчика для PAL16R4 на языке PALASM.
Рис. 8. Пример реализации 4-х битного счетчика для PAL16R4 на языке PALASM.

В 1985 году компания Lattice Semiconductor представила свой, расширенный и углубленный вариант микросхемы PAL, которую также можно было перепрограммировать многократно электрическим путем по технологии ЭППЗУ (EEPROM). Эта микросхема имела некоторые структурные усовершенствования: в структуру были введены так называемые «макроячейки» («macrocels») содержащие помимо логических «И» и «ИЛИ» еще регистры (D-триггеры) и мультиплексоры для обхода регистров. Также появилась возможность организовывать обратные связи — подавать сигнал с выхода триггера на вход этой же или других ячеек. Схемы программируемой логики такого типа получили название «Generic Logic Array» (GAL). На рис 9. приведена структурная схема «макроячейки» Lattice GAL22V10, сама микросхема содержала десять таких «макроячеек» (22 входа и 10 выходов).

Рис. 9. Структура макроячейки (Output Logic Macro Cell) микросхемы GAL22V10.
Рис. 9. Структура макроячейки (Output Logic Macro Cell) микросхемы GAL22V10.

Микросхема Lattice GAL22V10 была и остается очень популярной в схемах управления и автоматики, а также в качестве связывающей логики (glue logic). Микросхема производилась до 2010 года, а её аналог Atmel ATF22V10 производится и по сей день. Для программирования этой микросхемы был разработан специализированный HDL язык CUPL и среда разработки Atmel WinCUPL. Эта среда позволяет синтезировать из кода на языке CUPL простые цифровые схемы для большого ряда PAL/GAL микросхем или экспортировать его в формате PALASM для применения в других программных продуктах.

Недавно, занимаясь «цифровым ретро», в процессе которого мне требовалось отображать состояние шины адреса и данных для 8-ми битной машины, я с удивлением для себя обнаружил, что в стандартном наборе логики 7400 отсутствует такое полезное устройство, как дешифратор 4-х битовых двоичных чисел в их шестнадцатеричное представление для отображения на 7-ми сегментном индикаторе. Проблема быстро решилась с помощью микросхемы ATF22V10 и небольшого кусочка кода для CUPL, позаимствованного из репозитория Дуга Габбарда, за что ему огромное спасибо. Полный проект и схему подключения микросхемы GAL22V10 (она же ATF22V10) к 7-ми сегментному индикатору можно получить по ссылке: https://sourceforge.net/projects/dual-bcd-to-hex-7-seg-driver/?source=navbar

Добавлю, что опенсорсная реализация компилятора с языка CUPL/PALASM называется Portable GAL Assembler — GALasm и доступна в репозитории на Github-е. GALasm разработал Алессандро Зумо в начале 2000-х годов, но не смотря на такую «древность» его код прекрасно компилируется и работает в современных ОС на основе Linux и BSD.

Также хочу отметить, что на Хабре есть интересная статья «PAL, GAL и путешествие в цифровое Ретро» от пользователя @alecv о структурах и истории PLA, PAL и GAL, с примерами кода на PALASM.

Изделия PLA, PAL и GAL бурно развивались, количество вентилей (логических элементов) и «макроячеек» стремительно увеличивалось, добавлялись новые логические блоки, и позже такие устройства стали называть Programmable Logic Device (PLD) и Complex Programmable Logic Device (CPLD), а в русскоязычной литературе — «Программируемые Логические Устройства». Подавляющее большинство ПЛУ (CPLD) имеют встроенную «перепрошиваемую» память, определяющую связи элементов внутри микросхемы. Так как процесс перепрошивки подразумевает восстановление и повторное прожигание «фьюзов», то количество итераций записи в такие устройства ограничено — от нескольких десятков до нескольких сотен циклов.

Параллельно с ПЛУ шло развитие программируемой логики и по другому пути. Вместо отдельных вентилей с программируемыми связями, стали использовать массив достаточно крупных блоков логики — «Configurable Logic Blocks» (CLBs), «Logical Units» (LUs), «Logical Cells» (LCs) или «макроблоков». Терминология здесь сильно разнится и зависит от фантазии продаванов конкретного производителя микросхем. Каждый макроблок в таких изделиях имеет в составе программируемую таблицу преобразования («lookup table» или LUT) с двумя, тремя или четырьмя входами, а так же регистр и набор мультиплексоров. Макроблоки располагали в прямоугольную структуру, поверх которой располагалась матрица коммутации. Схема коммутации между входами и выходами макроблоков в таких устройствах задается ячейками статической памяти, выходы которых подключаются к транзисторным ключам матрицы коммутации. Для того чтобы внутри устройства образовалась какая-то схема соединений между макроблоками и сами макроблоки приобрели определенные свойства, необходимо записать данные в требуемые статические ячейки памяти, т. е. загрузить в микросхему битовый поток конфигурации. Такие устройства получили название «Field Programmable Gate Array» (FPGA), в русскоязычной литературе — «Программируемые Логические Интегральные Схемы» (ПЛИС). Структурная схема макроблока (логической ячейки) классической микросхемы ПЛИС приведена на рисунке 10 ниже.

Рис. 10. Типовая структура «логического блока» классической ПЛИС на основе 4-LUT. На схеме: 3-LUT — трехвходовая таблица преобразования, FA — полный сумматор, DFF — D-триггер (регистр).
Рис. 10. Типовая структура «логического блока» классической ПЛИС на основе 4-LUT.
На схеме: 3-LUT — трехвходовая таблица преобразования, FA — полный сумматор, DFF — D-триггер (регистр).

Устройства из массива макроблоков такой структуры имеют определенные преимущества над массивом логических вентилей PLA/PAL и PLD. Если посмотреть на структуру макроблока, то можно определить сходство макроблока с элементарной синхронной схемой, изображенной на рис. 2, которая лежит в основе RTL — комбинационная логика на входе (LUT + сумматор) и регистр (DFF) на выходе. А значит из таких кирпичиков можно складывать синхронные схемы обработки данных и вычислительные блоки.

Пару слов про LUT («lookup table»). LUT представляет собой аппаратную программно реконфигурируемую булеву функцию (это может быть «И», «ИЛИ», «НЕ» или любая другая). Реализуется LUT обычно в виде иерархической структуры мультиплексоров, входы нижнего уровня которых подключены к ячейкам статической памяти, задающим выполняемую функцию, а входы управления — к входным сигналам, над которыми эта функция применяется.

Рис. 11. Типовая схема 4-LUT для многих микросхем ПЛИС.
Рис. 11. Типовая схема 4-LUT для многих микросхем ПЛИС.

На схеме рис. 11 приведена типовая схема четырехвходовой «lookup table» (4-LUT). Линии управления мультиплексорами I0, I1, I2 и I3 являются входными двоичными сигналами для блока 4-LUT, а логическая операция, которая над ними выполняется, задается шестнадцатью битами статической памяти (SRAM) обозначенных на схеме как BIT[0]...BIT[15], состояние которых определяется в момент загрузки конфигурации в микросхему ПЛИС, т. е. в момент её инициализации. Результат операции подается на выход O. На такую таблицу 4-LUT можно посмотреть и с другой стороны — это просто блок статической памяти, который содержит шестнадцать однобитовых ячеек, адресуемых линиями I0...I3. Из схемы видно, что 4-LUT состоит из двух 3-LUT блоков, соединенных мультиплексором верхнего уровня, каждый 3-LUT состоит их двух 2-LUT, также объединенных мультиплексором уровнем пониже, и так далее. Современные ПЛИС могут содержать разное количество LUT разной структуры, вплоть до 7-LUT.

Отличительной особенностью микросхем ПЛИС от ПЛУ (PLA/PAL/PLD/CPLD) долгое время была необходимость в наличии внешнего блока памяти, обычно NOR flash, для сохранения битового потока конфигурации ПЛИС, который должен быть загружен в микросхему ПЛИС при её включении в процессе инициализации. Этим же и определялся основной их недостаток — ПЛИС требует некоторого весьма существенного (до 100 мс) времени для выхода на рабочий режим. В этом смысле микросхемы ПЛУ более удобны, так как в них конфигурация внутренних связей всегда сохраняется состоянием прошитых «фьюзов» даже при отключении питания, а значит ПЛУ готовы к работе почти мгновенно после подачи питания или сброса. В некоторых случаях микросхемы ПЛУ использовались для загрузки конфигурации в микросхемы ПЛИС.

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

Первопроходцем в создании микросхем ПЛИС принято считать компанию Altera (ныне входит в состав Intel). Altera начала в 1983 году с создания достаточно простых PLA/PLD устройств, которые эволюционировали в сложные FPGA. Другим известным производителем микросхем ПЛИС является компания Xilinx (принадлежит AMD), которая вышла на рынок со своими PLD изделиями в 1985 году и на данный момент Xilinx является лидером рынка программируемой логики. Если я не ошибаюсь, то на момент написания данной статьи рекордсменом по числу макроблоков на одном кристалле является микросхема Virtex UltraScale+ VU19P пр-ва Xilinx, которая содержит умопомрачительные 9 миллионов «logical cells» и огромное количество разнообразных «hard blocks».

Рис. 12. AMD/Xilinx Virtex UltraScale+ VU19P. 2020г.
Рис. 12. AMD/Xilinx Virtex UltraScale+ VU19P. 2020г.

Надо отметить, что в современных микросхемах ПЛИС помимо макроблоков (логических блоков, LUT-ов) присутствуют т. н. «hard blocks» — блоки специализированной логики и обработки данных, такие как: блоки преобразования частот (PLL), умножители и делители (MUL/DIV) и целые ALU, блоки обработки сигналов (DSP), блоки распределенной памяти (BRAM), блоки динамической памяти (SDRAM), сериализаторы-десериализаторы (SERDES), блоки цифровых интерфейсов и шин (DDR, PCIe, USB), высокоскоростные трансиверы и т. д. С некоторых пор в микросхемы ПЛИС начали активно интегрировать вычислительные ядра ARM.

Таким образом, современные ПЛИС — это сложные гибридные многофункциональные устройства, средствами которых можно решать тяжелые специализированные задачи по высокоскоростной обработке сигналов; строить высокопроизводительные вычислительные системы для симуляции сложных физических процессов или симуляции других специализированных цифровых и аналоговых схем (т. н. ASIC - «Application Specifiс Integrated Circuits») и их верификации при разработке и производстве.

По некоторым данным (Wikipedia), при симуляции ASIC средствами ПЛИС исходят из следующих эмпирических соотношений: площадь симулируемого кристалла будет в 40 раз меньше выбранной для симуляции микросхемы ПЛИС, скорость симуляции составит примерно 1/3 от скорости работы ASIC, а потребляемая мощность ПЛИС будет в 12 раз больше.

Также отмечу, что ряд производителей микропроцессоров (AMD, Intel), начали внедрять в свои высокопроизводительные микропроцессорные изделия «материю» ПЛИС, связь с которой обеспечивается специализированными высокоскоростными интерфейсами внутри одного кристалла. Как применять на практике такие изделия и для решения каких именно задача — мне пока не понятно.

3. Производители микросхем ПЛИС

Я буду использовать термин ПЛИС (или англоязычный вариант — FPGA) как собирательный для всех видов программируемой логики, основанной на макроблоках или макроячейках, так как в современной действительности грань между CPLD и FPGA устройствами сильно размыта, а принципы работы с ними мало чем отличаются.

Прежде всего надо сказать, что каждый производитель микросхем ПЛИС разрабатывает и распространяет свой инструментарий (toolchain или IDE или просто «тул») — целую среду разработки, позволяющую выполнить всё: от разработки схем на нескольких HDL языках (SystemVerilog и VHDL поддерживаются всеми), до верификации, симуляции, визуализации, синтеза и заливки полученной конфигурации. Но важно понимать, что этот инструментарий поддерживает синтез только для микросхемы ПЛИС конкретного производителя и, как правило, является отдельным коммерческим продуктом, который реализуется за отдельные, весьма серьезные деньги. Разумеется, существуют условно-бесплатные ограниченные лицензии на эти тулы. Эти лицензии распространяемые в исключительно «образовательных целях» вполне пригодны не только для обучающихся, но и для вполне опытных разработчиков. Однако, если Вы пожелаете использовать какие-то специфичные «hard blocks» присутствующие в выбранной для Вашего дизайна микросхеме ПЛИС, то скорее всего придется раскошелиться.

Еще один важный момент состоит в том, что на рынке присутствует с десяток производителей микросхем ПЛИС, каждый предлагает огромный модельный ряд своих изделий. Если в рамках одного производителя, точнее даже в рамках одной серии микросхем от одного производителя, присутствует хоть какая-то совместимость, то между микросхемами и тулами разных производителей совместимости нет никакой, а порой даже находятся отличия в синтаксисе HDL языков (несмотря на то, что есть стандарты). Иными словами, если разработчик пристрастился к продукции, скажем, производства Xilinx, то перейти на использование и перенести свои разработки на изделия Alter-ы будет катастрофически непростой задачей. Проблемой здесь являются эти самые «hard blocks», которые специфичны для конкретной микросхемы и конкретного тула конкретного производителя. «Hard blocks» очень сильно упрощают разработку, а во многих случаях без них вообще нельзя обойтись. Для части таких блоков у разных производителей могут быть эквивалентные замены (как например блоки PLL и SERDES есть у всех), но это не значит, что при переходе от одного вендора к другому вы сможете взять и просто перекомпилировать (пересинтезировать) свой дизайн с использованием других блоков. Нет, вам придется хорошо поработать руками и головой — переписать код и плотно протестировать его.

Далее я перечислю список известных производителей микросхем ПЛИС, некоторые из популярных моделей, а также среды разработки (тулы).

Известна следующими линейками микросхем ПЛИС: Vertex (топовые), Kintex (среднего уровня) и Artix — для тех кому попроще. Также широко известна серия гетерогенных изделий Zynq-7000 содержащая целую систему-на-кристалле с вычислительными ядрами ARM в комбинации с «фабрикой» ПЛИС.

Широко известна также серия «классических» ПЛИС микросхем SPARTAN 2/3/4/5/6/7, часть из которых уже снята с производства.

Основной тул: Vivado Design Suite.

2. Atera, Inc. Куплена и поглощена компанией Intel в 2015г, но в октябре 2023г Intel заявил, что собирается выделить бизнес по производству ПЛИС в отдельное независимое подразделение с выходом на IPO.

Известна следующими сериями микросхем ПЛИС: MAX (CPLD); Cyclon I, II, III, IV — классические ПЛИС; Stratix — ПЛИС с высокоскоростными интерфейсами; Cyclon V — гетерогенные с ядрами ARM Cortex-A9; Топовые изделия: Cyclon 10, Arria 10, Stratix 10.

Основной тул: Intel Quartus Prime. Quartus Prime Lite Edition доступен для скачивания бесплатно, но требуется регистрация и получение лицензии.

Устаревший, но имеющий очень широкое хождение тул Altera Quartus II, для которого есть бесплатная версия — Quartus II Web. Если не ошибаюсь, Quartus II работает только с микросхемами, произведенными до 2015г.

3. Microsemi Corporation (бывшая Actel), в 2018г поглощенная компанией Microchip Technology Inc. Специализируется на радиационно-стойких ПЛИС для авиации и космоса, в связи с чем изделия этого производителя содержат небольшое количество логических блоков. Редкий гость в наших краях.

Известна следующими сериями микросхем: IGLOO, PolarFire и PolarFire SoC — содержит ядра RISC-V.

Основной тул: Libero SoC Design Suite

4. Lattice Semiconductor, Inc. Один из старейших производителей микросхем ПЛИС на рынке (основана в 1983г). Производит классические ПЛИС малой и средней плотности (до 100K LUs), низким энергопотреблением и со встроенной конфигурационной памятью.

Известна следующими сериями микросхем: iCE40, ECP5 и ECP5-5G, MachXO (CPLD). Все эти серии микросхем имеют широкое хождение среди студентов и любителей самоделок и на то есть особая причина — поддержка открытым тулчейном Yosys, но об этом позже.

Основной тул: Lattice Diamond. Предоставляет возможность запросить бесплатную краткосрочную лицензию для «образовательных целей».


5. Efinix, Inc. Относительно молодой производитель (основана в 2012 году) с китайскими корнями. Специализируется на микросхемах ПЛИС малой, средней и высокой плотности — 4K-500K LEs. Также редкая птица в наших краях.

Известна следующими сериями микросхем: Trion (T8) и Trion Titanium — содержит ядра RISC-V.

Основной тул: Efinity Software. Доступен бесплатно для скачивания, выдается лицензия на 1 год сопровождения (бесплатные апгрейды) при приобретении комплекта разработчика Xyloni Development Kit.

6. GOWIN Semiconductors. Еще один производитель с китайскими корнями. Специализируется на микросхемах ПЛИС малой и средней плотности, но со своей «изюминкой». В виду своей дешевизны и доступности с Aliexpress, изделия этого производителя имеют широкое хождение в среде самоделкиных. Популярность ПЛИС этого производителя растет. Доступен в России.

Известен следующими микросхемами: LittleBee GW1N (от 1K до 8K LUs), Arora GW2A-18 (20К LUs) и GW2A-55 (54K LUs). Эти ПЛИС также содержат большое количество блоковой статической памяти (BSRAM), а серия GW1NR содержит до 64 Mbits динамической (SDRAM) памяти и доступную пользователю Flash память. Поддерживаются открытым тулчейном Yosys.

Основной тул: GOWIN EDA. Доступен бесплатно, но требуется регистрация и получение лицензии. Доступно в России через посредника.

Под «классическими» я понимаю такие ПЛИС, которые внутри построены на регулярных макроблоках, структура которых приведена на рис. 10 или близкая к ней. Современные изделия от Xilinx и Altera сильно отошли от этой изящной структуры.

4. Синтез цифровых схем для ПЛИС

Не ошибусь если скажу, что появления микросхем программируемой логики PLD/CPLD и FPGA, а это середина-конец 1980-х, произошло в самый подходящий для этого момент. К этому времени уже существовали различные HDL языки; разработка микросхем с использованием HDL и верификация их дизайнов перед запуском производства широко входила в практику. Поэтому для конфигурирования программируемой логики почти сразу начали использовать HDL языки. Для этого был создан инструментарий, позволяющий выполнять «синтез» схем, т. е. преобразование схемы с языков HDL в набор битов конфигурации микросхемы ПЛИС, реализующий заданную схему.

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

Этап 0. Составление спецификации и эталонной модели

Как и при разработке программного обеспечения, разработка цифровых схем начинается с постановки задачи. Для этих целей проектировщик описывает свойства будущего изделия сначала в виде структурной диаграммы (рис. 13), которая постепенно, сверху вниз детализируется (принцип «top-down design») с использованием микроархитектурных диаграмм (см. пример на рис. 14). Из микроархитектурных диаграмм прослеживаются все присутствующие в изделии сигналы, регистры и логические блоки. По микроархитектурным диаграммам строятся временные диаграммы, описывающие все возможные состояния входных и выходных сигналов и их изменение с течением времени.

Рис. 13. Блочная диаграмма иллюстрирующая структуру микропроцессора Intel 80386DX.
Рис. 13. Блочная диаграмма иллюстрирующая структуру микропроцессора Intel 80386DX.
Рис 14. Микроархитектурная диаграмма однотактового микропроцессора RV32I. На микроархитектурной диаграмме просматриваются все сигналы, регистры и комбинационные блоки устройства.
Рис 14. Микроархитектурная диаграмма однотактового микропроцессора RV32I. На микроархитектурной диаграмме просматриваются все сигналы, регистры и комбинационные блоки устройства.
Рис 15. Пример временной диаграммы при доступе в память.
Рис 15. Пример временной диаграммы при доступе в память.

Когда готов набор микроархитектурных и соответствующих временных диаграмм, производят создание «эталонной модели» (golden reference model). Для этого часто используют обычные языки программирования или язык SystemC, который представляет собой набор классов для C++. Смысл этого этапа состоит в том, чтобы, во-первых промоделировать систему в целом и понять какие у неё могут быть свойства, какая теоретически достижимая производительность и вообще проверить идею. Во-вторых, чтобы использовать полученную эталонную модель далее для верификации дизайна.

Этап 1. Выбор ПЛИС

Имея на руках спецификации, диаграммы и модель будущего изделия, разработчик определяется с производителем и моделью ПЛИС, на которой он собирается решать поставленную задачу. В этом деле очень много субъективизма и личных предпочтений. Кто-то исторически привык к ПЛИС-ам Altera; кто-то считает, что изделия Xilinx — лучшие, так как микросхемы этого производителя на данный момент самые насыщенные по функционалу; кто-то довольствуется аскетизмом классических ПЛИС от Lattice или дешевизной GOWIN. Главное, чтобы ресурсов ПЛИС, т. е. число LEs, LUTs, блоков памяти и прочих «строительных блоков» имелось с хорошим (читай — двойным) запасом. А для того, чтобы как-то оценить количество требуемых ресурсов, нужны микроархитектурные диаграммы.

Совет от себя лично — для начинающих рекомендую обратить внимание на микросхемы ПЛИС Lattice iCE40 и ECP5, а также GOWIN GW1N и GW2A (LittleBee и Arora).

Этап 2. Приобретение средств разработки

Разработчик определяется с приобретением средств разработки (тулом) и его установки на рабочий ПК (или на сервер). Все современные тулы поддерживают ОС Windows и Linux и могут быть использованы как в интерактивном режиме (IDE), так и в пакетном (командная строка и набор скриптов). Параллельно с этим приобретается отладочная плата (или несколько разных), содержащая выбранную модель микросхемы ПЛИС и устройство для её программирования — как правило, JTAG программатор. Многие отладочные платы сейчас уже идут со встроенным программатором с USB разъемом, что очень удобно. Если для Вашей модели ПЛИС вдруг не оказалось отладочной платы со встроенным программатором, то стоит приобрести отдельный JTAG адаптер, например USB Blaster — этот программатор (JTAG адаптер) поддерживается огромным спектром как коммерческих тулов, так и «опенсорсных» (СПО). Вообще, подойдет любой USB<->Serial адаптер на основе популярной микросхемы FTDI FT2232.

Этап 3. Составление плана распиновки

Изучается спецификация (даташит) на выбранную микросхему ПЛИС и создается описание её внешних выводов — каждому внешнему сигналу назначается один или несколько pad-ов (пинов). Это описание представляется в текстовом файле, обычно имеющего расширение .lpf или .cst. Для создания этого файла в коммерческих тулах имеются специальные средства, позволяющие выбрать модель микросхемы из списка поддерживаемых и визуально распределить сигналы по выводам корпуса микросхемы. Использование таких визуальных средств не всегда удобно, иногда проще создать и редактировать текстовый файл вручную, а параметры pad-ов подсмотреть в даташите на конкретную микросхему.

Ниже, для примера, я приведу небольшую выдержку LPF файла из своего проекта с микросхемой ПЛИС Lattice серии ECP5 (25K LEs). Файл karnix_hub12_cabga256.lpf, структуру которого мы рассмотрим более подробно чуть ниже, содержит примерно следующее:

PORTFREQUENCY PORT "io_clk25" 25.0 MHz; 
LOCATE COMP "io_clk25" SITE "B9"; 
IOBUF PORT "io_clk25" IO_TYPE=LVCMOS33; 

LOCATE COMP "io_rgb[0]" SITE "A13";             # LED0
IOBUF PORT "io_rgb[0]" IO_TYPE=LVCMOS33;
LOCATE COMP "io_rgb[1]" SITE "A14";             # LED1 - WORK
IOBUF PORT "io_rgb[1]" IO_TYPE=LVCMOS33;
LOCATE COMP "io_rgb[2]" SITE "A15";             # LED2 - TEST
IOBUF PORT "io_rgb[2]" IO_TYPE=LVCMOS33;
LOCATE COMP "io_rgb[3]" SITE "B14";             # LED3
IOBUF PORT "io_rgb[3]" IO_TYPE=LVCMOS33;
LOCATE COMP "io_gpio[0]" SITE "L1";             # HUB_00
IOBUF PORT "io_gpio[0]" IO_TYPE=LVCMOS33;
IOBUF PORT "io_gpio[0]" PULLMODE=NONE DRIVE=16;

…

Этап 4. Написание кода

Разработчик пишет код цифровой схемы на языках HDL — традиционно на Verilog (SystemVerilog) или VHDL. Нетрадиционно — на Chisel, SpinalHDL или nMigen/AmaranthHDL. Существует приличное количество экзотических HDL языков, но почти все они в конечном счете сводятся к генерации кода на языках Verilog или VHDL.

Очень часто при разработке используются библиотечные или приобретенные функциональные блоки (IP блоки) — большие куски кода на HDL, которые требуют адаптации и интеграции в новый дизайн. Языки HDL позволяют создавать (описывать) блоки параметризованными, что сильно упрощает повторное их использование.

Этап 5. Верификация и симуляция

Код отлаживается, верифицируется и симулируется (реализуется потактово). Производится это либо встроенными средствами коммерческого тула от поставщика микросхем ПЛИС, либо приобретенным коммерческими тулами специального назначения, как-то ModelSim и Questa. Крупные компании по разработке микросхем часто используют свои доморощенные тулы для верификации и симуляции, как в примере с Intel 80386 — помните слова Кена Ширриффа про sed, awk и grep ? В академической среде, а так же среди самоделкиных и небольших компаний разработчиков имеют широкое хождение «опенсорсные» тулы, такие как Icarus Verilog и Verilator. Весь процесс разработки, верификации и симуляции итеративно повторяется до тех пор, пока не будет получен дизайн, удовлетворяющий требованиям спецификации или соответствующий «эталонной модели», в которой, кстати, тоже могут быть ошибки.

Следует заметить, что в микроэлектронной промышленности широко проповедуется принцип «Design for Verification» — некий свод правил для инженеров разработчиков и проектировщиков, обеспечивающий выход годной продукции с первого раза. Т.е. дизайн поступает на фабрику только тогда, когда пройдены все этапы верификации и симуляции и есть твердая уверенность в отсутствии ошибок. Причем, «фабы» тоже проверяют поступающие к ним дизайны на соответствие уже своим правилам — «Design Rule Check» (DRC check).

Этап 6. Синтез

Из полученного кода на HDL, средствами выбранного тула производится синтез т. н. netlist-а. Netlist — это файл (обычно формата EDIF, BDIF или JSON) описывающий представление разработанной цифровой схемы из «атомарных» блоков, которые присутствуют в выбранной микросхеме ПЛИС. Такие блоки обычно называют «Standard cells». Синтезированный netlist при желании можно визуализировать в виде цифровой схемы.

Этап 7. Оптимизация и STA

Далее производится оптимизация netlist-а — из него удаляются лишние блоки, типовые структуры комбинируются и замещаются на упрощенные/эквивалентные или на специальные «hard» блоки, специфичные для выбранной ПЛИС. Вся эта процедура выполняется автоматически специальными утилитами, входящими в состав тула. Существует множество алгоритмов оптимизации, они вызываются один за другим из скриптов, повторяются в цикле с разными параметрами и т. д. Опытные разработчики предпочитают писать свои скрипты для оптимизации своих дизайнов.

На этапе оптимизации netlist-а подключаются алгоритмы «Static Timing Analisys» (STA) которые достаточно точно позволяют рассчитать временные задержки распространения сигналов в синтезированной схеме, а также позволяют выполнить ряд оптимизаций по улучшению этого показателя (т. е. уменьшить задержки). Часто бывает так, что на этом этапе дальнейшая обработка netlist-а невозможна, так как в нём присутствуют логические цепочки, рассчитанное время распространения сигнала в которых превышает заданное при разработке схемы значение (нарушение спецификации). В этом случае разработчику приходится возвращаться и перепроектировать схему — переписывать код на HDL, применять другие решения, отказываясь от громоздких комбинационных схем или оптимизировать их вручную разбивая регистрами (т. е. выполнять «конвейеризацию»), после чего повторять весь процесс верификации, симуляции, синтеза и оптимизации.

Этап 8. Расстановка и трассировка

Оптимизированный netlist поступает на вход следующей утилите, которая выполняет процесс расстановки блоков по местам и маршрутизацию сигнальных линий их связывающих, это т. н. процесс «place and route» (PnR или «плэйсер»). В этом процессе каждому блоку из netlist-a определяется фиксированное место внутри микросхемы ПЛИС, а блоки связываются между собой доступными средствами матрицы коммутации, к которой также подключаются внешние вывода (pad-ы) микросхемы и тактовые сигналы. На этом этапе происходит очень тяжелый и затратный по времени и ресурсам процесс оптимизации, но уже не структурной, а топологической — утилита PnR ищет оптимальное расположение блоков и их связей, так чтобы не нарушить дизайн и его временные характеристики. Результатом работы утилиты плэйсера является топологическое представление схемы внутри ПЛИС (тоже файл формата EDIF или JSON). После работы плэйсера временные характеристики дизайна, как правило, улучшаются, но бывает и наоборот — всё становится только хуже и разработчику приходится возвращаться к редизайну на HDL. Иногда проблемы с расстановкой можно решить изменением числа «рандомизации» (seed), так как алгоритмы плэйсера не являются строго детерминированными и часто основываются на случайных последовательностях. Т.е. покрутив «seed» несколько раз, можно попытаться добиться результата, удовлетворяющего требованиям STA, и это будет работать.

Надо отметить, что трассировка тактовых сигналов, как правило, производится отдельно от остальных цифровых сигналов, и для них в микросхемах ПЛИС предусматриваются специальные глобальные линии («global nets») с минимальными задержками и искажениями фронтов. Если в дизайне используются генерируемые внутри него тактовые сигналы, то такие сигналы требуют особого внимания — их требуется вывести на глобальные линии, для чего используется специальный синтаксис и специализированные «hard» блоки, предназначенные для этой цели. Также глобальными линиями принято трассировать сигналы сброса (resets) и некоторые общие для множества блоков схемы сигналы разрешения (enable signals). Иногда утилита синтеза автоматически распознает такие сигналы и маркирует их специальным образом, чтобы плэйсер на этапе размещения и трассировки использовал глобальные линии, но это не всегда работает верно и требует внимания разработчика.

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

В больших и сложных дизайнах для FPGA (и особенно для ASIC) обычно присутствует еще один этап, предшествующий вызову плэйсера, который называется «floorplanning» и в процессе которого для плэйсера задают набор правил и ограничений. Например, разработчик может посчитать необходимым размещение каких-то отдельных блоков рядом друг с другом или возле выходных сигналов (возле pad-ов) или поближе к линиям питания. Но в большинстве случаев этот процесс планирования расстановки проистекает автоматически внутри плэйсера и дает приемлемый результат.

Этап 9. Генерация битстрима

Завершающим этапом синтеза является генерация «битстрима» — последовательности бит которую можно загрузить в микросхему ПЛИС. Для этого используется отдельная утилита в составе тула, она принимает на вход JSON файл от плэйсера и генерирует на выходе двоичный файл (обычно с расширением .bit, .bin или .fs) готовый к загрузке в ПЛИС. В большинстве случаев это чисто технический этап выполняемый автоматически и не требующий взаимодействия с разработчиком.

Этап 10. Загрузка в ПЛИС и проверка на железе

После того как разработчик получил «битстрим», его можно и нужно загрузить в NOR flash память на отладочной плате чтобы микросхема ПЛИС считала его при следующей инициализации. Производится это с помощью JTAG программатора (см. выше) и специальной утилиты. В коммерческих тулах такая утилита входит в поставку. Существуют и «опенсорсные» утилиты, например openFPGALoader.

Очень частно, при активной отладке дизайна и прогонке его «на железе», имеет смысла загружать битстрим не в NOR flash, а прямо в конфигурационные регистры SRAM микросхемы ПЛИС. Это позволяет, во-первых, экономить число операций записи во flash, во-вторых, это существенно быстрее и позволяет экономить время разработчику. Но тут надо не забывать о том, что при сбросе (инициализации), микросхема ПЛИС всегда считывает конфигурацию из NOR flash, т. е. заново перезапишет содержимое своей конфигурационной памяти, а загруженный в неё ранее битстрим будет потерян. Если разработчик случайно выпускает этот момент из вида, то это часто приводит к разного рода «необъяснимым» эффектам в поведении отлаживаемого дизайна. Поэтому, мой небольшой совет начинающим — если что-то идет не так, перезалейте Ваш битстрим в NOR flash и выполните сброс по питанию!

5. Yosys Open SYnthesis Suite

Про существование программируемой логики я лично узнал еще в конце 90-х, будучи студентом, но микросхему ПЛИС живьём увидел много лет спустя - в 2010 или где-то около того. Если не ошибаюсь, то была микросхема про-ва Altera серии Cyclon II на небольшой плате в комплекте с кварцевым генератором и стабилизаторами питания. Больше на плате ничего не было, все выводы ПЛИС были попросту выведены на штыревые разъёмы, расположенные по четырем сторонам платы. Разумеется, у меня возникло желание попробовать «запрограммировать» её, но я тут же столкнулся с рядом трудностей. Во-первых, требовался какой-то JTAG программатор, а эти устройства в то время стоили баснословных денег (и софт для них тоже). Во-вторых, всё сообщество российских (да и западных) разработчиков-плисоводов плотно сидело на ОС Windows и интенсивно ёрзало мышами по экрану. Для меня, человека, привыкшему работать в командной строке удаленно по SSH и люто ненавидящему не только «винду», но и весь проприетарный софт, это вызывало стойкое отвращение (разумеется, мне часто приходилось использовать «винду», но только лишь для того, чтобы запустить PuTTY). Я несколько раз выкачивал Quartus II под Linux и пытался устанавливать его на сервер разработки, но сталкивался с какими-то нерешаемыми трудностями — понять, как пользоваться этим мульти-гигабайтным тулом из shell-а было не просто, так как детальной инструкций никто не написал, а все форумы в то время были завалены инструкциями со скриншотами окошек. Короче, ни Quartus, ни Vivado, ни любой другой проприетарный тул я осилить так и не смог. А может быть, просто не захотел.

Зато в 2018 году я узнал про существование полностью open source решения для синтеза цифровых схем и это сильно прибавило мне энтузиазма. Интерес к теме программирования ПЛИС также подогревался начавшей бурно цвести темой RISC-V (по-русски читается как «риск-5», а не как вакцина от Ковида) — полностью открытой микропроцессорной архитектуре, вычислительные ядра которой уже можно было синтезировать для ПЛИС. Так я повторно начал погружаться в тему, но уже совсем c другим, нетрадиционным подходом.

Основная утилита в этом опенсорсном решении, Yosys, представляет собой тул для выполнения синтеза, т. е. преобразования описания цифровой схемы из текста на языке Verilog в netlist с последующим выполнением процедур оптимизации, используя библиотеку Berkeley ABC. Если быть более точным, то Yosys — это всего лишь фронтэнд, преобразующий текст на языке Verilog (позже добавили поддержку VHDL) в структуры, которые может обрабатывать библиотека ABC, а вся магия происходит именно в ней. Библиотека ABC появилась очень давно и уходит своими корнями в 1980-е годы. Всё это время основное применение библиотека ABC находила в академических застенках для моделирования, симуляции и верификации, и лишь появление утилиты Yosys в 2014 году сделало доступным всю мощь ABC для широких масс. Вместе Yosys, ABC, NextPNR и еще ряд других открытых проектов устроили настоящую революцию в индустрии синтеза цифровых схем в конце 2010-х годов, где преобладало засилье закрытых, проприетарных, бесконечно раздутых и заоблачно дорогих программных пакетов с входом «только для своих».

И Yosys это не только для ПЛИС. На данный момент на основе цепочки утилит, во главе которых находится Yosys, построен пакет открытых программ OpenLane/OpenROAD, предназначенный для автоматизации разработки и автоматической трассировки микросхем (ASIC), с помощью которого любой желающий может полностью выполнить дизайн цифровой микросхемы и даже бесплатно заказать её в производство в составе «Multi Project Wafer». MPW — это способ производства, когда на одной кремниевой пластине («вафле») размещается несколько разных проектов с целью удешевления производства прототипов.

Интересно и то, что Yosys изначально это бакалаврская дипломная работа Клиффорда Вулфа, выполненная при работе над другим проектом.

Wol13] Wolf, Clifford: Design and Implementation of the Yosys Open SYnthesis Suite. 2013. – Bachelor Thesis, Vienna University of Technology.

Отличная документация, написанная Вулфом по утилите Yosys, находится тут.

Как я отметил выше, Yosys — это своего рода верхушка айсберга, под которой скрывается целая цепочка связанных воедино опенсорсных проектов с целью предоставить широкой общественности вариант законченного альтернативного решения для синтеза цифровых схем и не только. Ниже я попытаюсь кратко разобрать некоторые, самые важные, из них.

Arachne-pnr («паук»). Утилита-плэйсер и первая попытка реализовать полностью законченный опенсорсный тул синтеза для ПЛИС, автором является Коттон Сид. Сид связал проекты IceStorm и Yosys и дописал к ним недостающее звено — «плэйсер», в результате получил тул для синтеза под микросхемы ПЛИС Lattice iCE40. На данный момент «паук» больше не поддерживается, весь его функционал полностью перекрывается другим, более мощным инструментом — NextPNR.

IceStorm. Проект по документированию внутреннего устройства микросхемы ПЛИС Lattice iCE40. Проект выполнен Мэтиасом Лассером (Mathias Lasser) и всё тем же Клиффордом Вулфом в 2015 году.

Идея проекта следующая. Всем очень хочется иметь открытый тул для синтеза, но внутренняя структура микросхем ПЛИС тщательно скрывается их производителями «за семью патентами», что сильно препятствует созданию такого полезного тула. Не беда. Возьмем самую простую по структуре микросхему ПЛИС, возьмем проприетарный тул от производителя и будем синтезировать много «случайных» схем, а к полученному битстриму применим статистический анализ. Ну почти как арабский математик Аль-Кинди, который в 9-м веке придумал статистический метод взлома шифров. И это сработало! Первая микросхема ПЛИС, которую полностью документировали Мэт и Клифф, была iCE40 производства Lattice Semiconductors.

С результатами этой ошеломляющей работы Клифф выступил на регулярном съезде Der Chaos Computer Club (видео). В своем выступлении Клифф демонстрировал слайды, в которых сравнивал качество и скорость синтеза созданного ими инструмента с проприетарным. Результат потрясающий — несмотря на некоторое отставание в оптимальности использования ресурсов микросхемы, опенсорсный тул работал в разы быстрее проприетарного!

Чуть позже в рамках проекта Project Trellis была полностью документирована микросхема ПЛИС Lattice ECP5 со всеми её «hard» блоками. А это очень интересная по своим возможностям микросхема, позволяющая синтезировать в ней ядро RISC-V с MMU и гонять на этом ядре настоящий Linux.

Рис. 16. Фрагмент выступления Клиффорда Вулфа с демонстрацией эффективности созданного им инструмента для синтеза под ПЛИС Lattice iCE40. На слайде «Implementation time» - время выполнения трассировки.
Рис. 16. Фрагмент выступления Клиффорда Вулфа с демонстрацией эффективности созданного им инструмента для синтеза под ПЛИС Lattice iCE40. На слайде «Implementation time» - время выполнения трассировки.

Разумеется, идею Клиффа и Мэта тут же подхватило сообщество, и процесс не пошел, а полетел, моментально последовали проекты по документированию ПЛИС других производителей. Проекты IceStorm, Trellis, Oxide, Apicula, Mistral, X-ray и Chibi нацелились на реверс-инжиниринг и документирование популярных микросхем ПЛИС и на данный момент в разной степени готовности имеется документация на Lattice iCE40, ECP5 и Nexus; Altera Cyclon V; Intel MAX; Xilinx Artyx и UltraScale; Gowin LittleBee и Arora. Эта документация формализована и представляется в виде специализированной базы данных — ChipDB.

В этом видео Дэйвид Шах (David Shah) из Project Trellis рассказывает, как он и его команда зареверсили микросхему ПЛИС Lattice ECP5, как выглядит битстрим для этой ПЛИС и какие инструменты ему пришлось создать, чтобы проводить анализ («fuzzing») битстрима.

NextPnR — новый, продвинутый, универсальный (вендоро-независимый), timing driven, плэйсер с открытым исходным кодом, поддерживающий все микросхемы ПЛИС из ChipDB. NextPNR работает в полностью автоматическом режиме, принимает на вход результат работы синтезатора Yosys, а также файлы с описанием ограничений (LPF и ряд других) и выдает на выходе файл, по которому можно уже легко построить битстрим. Помимо этого, NextPNR может генерировать файлы в формате SVG с изображением схемы, полученной в результате размещения и трассировки, что может быть использовано для визуализации, отладки и дальнейшей ручной оптимизации дизайнов.

Рис. 17. Визуализация результата работы утилиты NextPNR при синтезе для микросхемы ПЛИС Lattice iCE40HX1K.
Рис. 17. Визуализация результата работы утилиты NextPNR
при синтезе для микросхемы ПЛИС Lattice iCE40HX1K.

openFPGALoader — универсальная утилита-программатор для загрузки битстрима. Поддерживает огромное количество устройств-программаторов, микроcхем ПЛИС и ПЛУ (не только тех, что присутствуют в ChipDB). Имеет возможность гибкой подстройки сигнальных линий JTAG, позволяя использовать программаторы, даже если их нет в списке поддерживаемых. Позволяет загружать битстрим как в NOR flash, с которой работает ПЛИС, так и в RAM самой ПЛИС. Может быть использована для прошивки большого количества различных моделей микросхем NOR flash памяти. Утилита openFPGALoader уже присутствует в репозиториях пакетов почти всех известных дистрибутивов Linux и *BSD.

На данный момент существует над-проект YosysHQ который покрывает все вопросы синтеза, верификации, симуляции, трассировки и создания документации для микросхем ПЛИС с использованием решений с открытым исходным кодом. Авторы и активные коммиттеры этого проекта организовали свою компанию (YosysHQ GmbH) и предлагают услуги консалтинга, а также ряд коммерческих продуктов на основе созданных ими опенсорсных решений.

6. Как установить утилиты тулчейна Yosys

Самый простой способ — это скачать и установить готовый пакет бинарных программ «OSS CAD Suite» подготовленный разработчиками из YosysHQ, который оформлен двумя способами: в виде docker контейнера и в виде тарболла. Бинарный пакет собирается каждую ночь (nightly builds) и выкладывается в репозиторий, поэтому тут нужно быть осторожным, чтобы случайно не попасть на «сломанную» версию пакета (если что-то пошло не так, то нужно попробовать более раннюю версию пакета). На данный момент «еженощно» формируются билды для нескольких платформ, вот выдержка из README репозитория:

linux-x64

Any personal Linux based computer should just work, no additional packages are needed to be installed on system to make OSS CAD Suite working. Distributed libraries are based on Ubuntu 20.04, but everything is packaged in such a way so it can be used on any Linux distribution.

darwin-x64

Any macOS 10.14 or later with Intel CPU should use this distribution package.

darwin-arm64

Any macOS 11.00 or later with M1 CPU should use this distribution package.

windows-x64

This architecture is supported for Windows 10 and 11, but older 64-bit version of Windows 7, 8 or 8.1 should work.

linux-arm

ARM based Linux devices such as Raspberry Pi 3, 4 or 400 can use this distribution package.

linux-arm64

ARM64 based Linux devices using 64bit CPU as in Raspberry Pi 4 and 400 (with 64bit version of OS installed), and also laptops like the MNT Reform 2 can use this distribution package.

linux-riscv64

RiscV-64 based Linux devices should use this distribtuion package, but please note that this is currently untested

Установка через docker, пожалуй, наиболее простой вариант, так как у YosysHQ имеется свой «Hub» на сайте Docker-а, на котором выкладываются docker файлы для всех имеющихся вариантов пакета. Это значит, что выкачать и установить контейнер, собранный для платформы linux-x64, можно командой вида:

$ docker pull yosyshq/cross-linux-x64

Установить из тарболла тоже не сложно. Достаточно сходить в репозиторий по ссылке:

https://github.com/YosysHQ/oss-cad-suite-build/releases/latest

и скачать требуемую версию пакета (.tgz файл объемом около 500МБ). После чего распаковать скачанный пакет в домашний каталог или любое другое удобное место, прописать пути к исполняемым файлам в $PATH и подготовить окружение:

Для Linux и MacOS:

	export PATH="<extracted_location>/oss-cad-suite/bin:$PATH"

Для FreeBSD:

	setenv PATH "<extracted_location>/oss-cad-suite/bin:$PATH"

далее подготовить переменные окружения специальным скриптом:

	source <extracted_location>/oss-cad-suite/environment

Для пользователей Windows:

из текущей оболочки:

	<extracted_location>\oss-cad-suite\environment.bat

или создать новое окно с оболочкой:

	<extracted_location>\oss-cad-suite\start.bat

Те, кому религия не позволяет использовать готовые бинарные пакеты, а я придерживаюсь именно этого течения, могут попробовать собрать весь стек утилит (или какую-то его часть) из исходных кодов. Мне удавалось успешно собирать почти весь стек под ОС ALT Linux p10 (x64) и под ОС FreeBSD 13.2 (amd64). Но это тема отдельного, очень длинного разговора.

После того как Вы установили Yosys тулчейн любым из способов, необходимо проверить, запускаются ли у Вас соответствующие утилиты:

rz@devbox:~$ yosys -V 
Yosys 0.36+58 (git sha1 ea7818d31, gcc 7.5.0-3ubuntu1~18.04 -fPIC -Os)

rz@devbox:~$ openFPGALoader -V 
openFPGALoader v0.11.0 

Для работы с ПЛИС Lattice ECP5 потребуются следующие утилиты:

rz@devbox:~$ nextpnr-ecp5 -V 
"nextpnr-ecp5" -- Next Generation Place and Route (Version nextpnr-0.6-157-g4a402519) 

rz@devbox:~$ ecppack --version 
Project Trellis ecppack Version 1.2.1-14-g488f4e7

rz@devbox:~$ ecpbram 
Project Trellis - Open Source Tools for ECP5 FPGAs 
ecpbram: ECP5 BRAM content initialization tool 

rz@devbox:~$ ecpprog --help 
Simple programming tool for FTDI-based Lattice ECP JTAG programmers. 
Usage: ecpprog [-b|-n|-c] <input file> 
ecpprog -r|-R<bytes> <output file> 
ecpprog -S <input file> 
ecpprog -t 

Для работы с ПЛИС Lattice iCE40 потребуются следующие утилиты:

rz@devbox:~$ nextpnr-ice40 -V 
"nextpnr-ice40" -- Next Generation Place and Route (Version nextpnr-0.6-157-g4a402519) 

rz@devbox:~$ icepack -h 
Usage: icepack [options] [input-file [output-file]] 

rz@devbox:~$ icebram 
Usage: icebram [options] <from_hexfile> <to_hexfile> 
icebram [options] -g [-s <seed>] <width> <depth> 
Replace BRAM initialization data in a .asc file. This can be used 
for example to replace firmware images without re-running synthesis 
and place&route. 

rz@devbox:~$ iceprog --help 
Simple programming tool for FTDI-based Lattice iCE programmers. 
Usage: iceprog [-b|-n|-c] <input file> 
iceprog -r|-R<bytes> <output file> 
iceprog -S <input file> 
iceprog -t 


Для работы с ПЛИС GOWIN LittleBee и Arora потребуются следующие утилиты:

rz@devbox:~$ nextpnr-gowin -V 
"nextpnr-gowin" -- Next Generation Place and Route (Version nextpnr-0.6-157-g4a402519)

rz@devbox:~$ gowin_bba -h 
usage: gowin_bba [-h] -d DEVICE [-i CONSTIDS] [-o OUTPUT] 

Make Gowin BBA 

rz@devbox:~$ gowin_pack -h 
usage: gowin_pack [-h] -d DEVICE [-o OUTPUT] [-c] [-s CST] [--allow_pinless_io] [--jtag_as_gpio] [--sspi_as_gpio] [--mspi_as_gpio] 
                  [--ready_as_gpio] [--done_as_gpio] [--reconfign_as_gpio] 

Pack Gowin bitstream 

Также для работы Вам потребуется ряд вспомогательных утилит:

rz@devbox:~$ make -v 
GNU Make 4.1 
Built for x86_64-pc-linux-gnu 

rz@devbox:~$ netlistsvg --version 
1.0.2 

rz@devbox:~$ xdot -h 
usage: xdot [-h] [-f FILTER] [-n] [-g GEOMETRY] [--hide-toolbar] [file] 
xdot.py is an interactive viewer for graphs written in Graphviz's dot language. 

Для проверки синтаксиса, верификации и симуляции Вам потребуются:

rz@devbox:~$ iverilog -V 
Icarus Verilog version 10.1 (stable) () 

rz@devbox:~$ gtkwave -V 
GTKWave Analyzer v3.3.117 (w)1999-2023 BSI 

rz@devbox:~$ verilator --version 
Verilator rev v5.020-157-g2b4852048 

Ну и последний, но важный момент. Чтобы утилита openFPGALoader могла иметь доступ к USB программатору в операционной системе Linux, требуется добавить набор правил для systemd. Для этого необходимо скачать файл с правилами https://github.com/trabucayre/openFPGALoader/blob/master/99-openfpgaloader.rules, поместить его в каталог /etc/udev/rules.d/ и перезагрузить операционную систему. Выполнять это нужно от имени суперпользователя (root-а).

7. Пример использования открытого тулчейна Yosys

Настало время попробовать Yosys в деле. По законам жанра здесь должен был бы следовать пример а-ля «hello, world», то есть мигание светодиодом или что-то в этом роде. Но я хотел бы направить читателя несколько другим путём и познакомить с очень интересным проектом по обучению широких масс основам цифрового синтеза и верификации. Этот проект так и называется «Школа синтеза цифровых схем», его идейным вдохновителем является Юрий Панчул (@YuriPanchul), наш соотечественник, который несколько десятков лет трудится в именитых электронных компаниях в «Кремниевой Долине» и имеет огромный опыт в этой сфере. У Юрия есть свой блог на Habr-е, где он регулярно выкладывает тематические статьи и посты о том, как устроена эта индустрия, какие в ней превалируют тенденции и какие актуальные проблемы присутствуют.

Со слов Юрия, по долгу службы ему часто приходится проводить собеседования молодых специалистов, претендующих на должность «digital hardware engineer» и в какой-то момент у него родилась идея сделать свой набор небольших лабораторных работ на языке SystemVerilog, чтобы использовать их, во-первых, на «собесе» для проверки знаний, а во-вторых — для повышения квалификации своих коллег из смежных ведомств. Этот набор лабораторных работ называется «basiсs-graphics-music» и он свободно доступен в репозитории Юрия на Github-е по ссылке: https://github.com/yuri-panchul/basics-graphics-music. Этот же набор лабораторных работ активно используется в «Школе синтеза».

Сам проект «basics-graphics-music» устроен так, что позволяет его использовать с большим спектром тулов (Quartus, Vivado, Gowin EDA), а также с большим количеством отладочных и учебных плат, содержащих микросхемы ПЛИС, и он по праву может называться первым портабельным (portable) репозиторием обучающих примеров для программирования ПЛИС. Для поддержки проект имеется список рассылки: portable-hdl-examples@googlegroups.com.

Еще из интересных особенностей данного сборника лабораторных работ можно отметить их идеологическую выверенность стиля кода и последовательность подачи материала. Лабораторные работы начинаются с демонстрации самых базовых принципов комбинационной логики (правила де Моргана) и описания машин-состояния (FSM) и доходят до весьма сложных (но небольших по объёму кода) примеров работы с VGA графикой, синтезированием и детектированием музыки.

Недавно в проект была добавлена поддержка тулчейна Yosys для нескольких плат с микросхемами ПЛИС Lattice и Gowin — работа, к которой я приложил свою руку.

Среди множества отладочных плат, поддерживаемых проектом «basics-graphics-music» присутствует, в том числе, разработанная мной и моими коллегами из ООО «Фабмикро» плата «ПИР СЦХ-254 Карно» («Karnaugh Interactive Extendable ASIC Simulation Board») предназначенная для обучения азам цифрового синтеза и экспериментов с синтезируемыми микропроцессорными ядрами RISC-V.

Рис. 18. Плата «Карно» - интерактивная расширяемая для синтезирования цифровых схем. 3D модель. Выполнена в KiCAD 7. OSHW проект.
Рис. 18. Плата «Карно» - интерактивная расширяемая для синтезирования цифровых схем. 3D модель. Выполнена в KiCAD 7. OSHW проект.

Это полностью «open source and open hardware» проект на базе ПЛИС Lattice ECP5 с ~25К логических блоков, содержит ряд интересных периферийных устройств: многоканальный ЦАП и АЦП, блок статической SRAM памяти объемом 512КБ, FastEthernet, HDMI, кнопки и светодиоды и кое-что еще. Плата «Карно» имеет встроенный JTAG программатор.

Плата выполнена в САПР KiCAD 7 и весь проект со схемой, трассировкой платы и Gerber файлами, доступен для скачивания с Github-а по ссылке: https://github.com/Fabmicro-LLC/Karnix_ASB-254 . Для демонстрации возможностей платы «Карно» я записал и выложил на YouTube новогоднее видео ссылка на которе дана в начале этой стетьи.

Далее я буду приводить примеры работы с тулчейном Yosys с её участием. Но как я отмечал выше, выбор платы не сильно принципиален, так как проектом «basics-graphics-music» поддерживаются ряд других, легко доступных отладочных плат, среди которых можно отметить OrangeCrab и TangNano-9K.

7.1. Установка и настройка «basics-graphics-music»

Итак, приступим к рассмотрению нескольких лабораторных работ. Клонируем репозиторий «basics-graphics-music» в любой удобный рабочий подкаталог:

rz@devbox:~$ git clone https://github.com/yuri-panchul/basics-graphics-music.git 
Cloning into 'basics-graphics-music'... 

rz@devbox:~$ cd basics-graphics-music
rz@devbox:~/basics-graphics-music$

Произведем настройку среды синтеза для нашей платы, для этого запустим скрипт check_setup_and_choose_fpga_board.bash следующим образом:

rz@devbox:~/basics-graphics-music$ bash check_setup_and_choose_fpga_board.bash 
check_setup_and_choose_fpga_board.bash: The currently selected FPGA board: karnix_ecp5_yosys. Please select an FPGA board amoung the following supported: 
1) alinx_ax4010                      25) karnix_ecp5_yosys 
2) arty_a7                           26) nexys4 
3) arty_a7_pmod_mic3                 27) nexys4_ddr 
4) basys3                            28) nexys_a7 
5) c5gx_audio                        29) omdazz 
6) c5gx_vga666                       30) omdazz_pmod_mic3 
7) c5gx_vga_pmod                     31) orangecrab_ecp5_yosys 
8) cmod_a7                           32) piswords6 
9) de0                               33) qmtech_c4_starter 
10) de0_cv                            34) rzrd 
11) de0_nano_soc_vga666               35) rzrd_pmod_mic3 
12) de0_nano_soc_vga_pmod             36) saylinx 
13) de0_nano_vga666                   37) saylinx_pmod_mic3 
14) de0_nano_vga_pmod                 38) tang_nano_20k 
15) de10_lite                         39) tang_nano_9k 
16) de10_lite_tm1638_arduino          40) tang_nano_9k_gowin_yosys 
17) de10_nano_vga666                  41) tang_primer_20k_dock 
18) de10_nano_vga_mister              42) tang_primer_20k_dock_alt 
19) de10_nano_vga_pmod                43) tang_primer_20k_dock_gowin_yosys 
20) de1_soc                           44) tang_primer_20k_lite 
21) de2_115                           45) zeowaa 
22) dk_dev_3c120n                     46) zeowaa_wo_dig_0 
23) emooc_cc                          47) zybo_z7 
24) ice40hx8k_evb_yosys               48) exit 
Your choice (a number): 25

Выберем плату «karnix_ecp5_yosys», для этого введем её порядковый номер из списка (в данном случае — 25) и нажмем «ввод», скрипт продолжит выполнение и выдаст следующие сообщения:

check_setup_and_choose_fpga_board.bash: FPGA board selected: karnix_ecp5_yosys 

check_setup_and_choose_fpga_board.bash: Created an FPGA board selection file: "/home/rz/basics-graphics-music/fpga_board_selection" 
Would you like to create the new run directories for the synthesis of all labs in the package, based on your FPGA board selection? We recommend to do this if you plan to work with Quartus GUI rather than with the synthesis scripts. [y/n]

Я нажму n, так как не использую Quartus. Скрип завершит работу с сообщением:

Configuring for Lattice ECP5... 
OK 
rz@devbox:~/basics-graphics-music$

Настройка среды готова. Посмотрим какие лабораторные работы нам доступны:

rz@devbox:~/basics-graphics-music$ ls labs
01_and_or_not_xor_de_morgan  08_shift_register     15_uart                             22_syst_ws 
02_mux                       09_7segment_word      16_round_robin_arbiter              23_i2s_synthesizer 
03_decoder                   10_hex_counter        17_geiger_muller_radiation_counter  24_i2s_music 
04_priority_encoder          11_note_recognition   18_pow5_single_cycle                50_crash_course 
05_7seven_segment_letter     12_snail_fsm          19_pow5_pipelined                   90_valid_ready_etc_preview 
06_vga                       13_music_recognition  20_pow5_pipelined_valid             91_systemverilog_homework_preview 
07_binary_counter            14_game               21_pow5_pipelined_valid_solution    common 

7.2 Лабораторная работа «01_and_or_not_xor_de_morgan»

Данная лабораторная работа показывает, как выглядят цифровые схемы, построенные только на комбинационной логике, выраженной на языке SystemVerilog.

Комбинационные схемы — это асинхронные (нетактируемые) схемы состоящие только из логических элементов, таких как «И», «ИЛИ», «НЕ», «исключающее ИЛИ» и т. д. Такие схемы подчиняются законам булевой алгебры, в том числе поддаются различным преобразованиям и упрощениям, к ним применимы правила де Моргана.

Зайдем в каталог с лабораторной работой и посмотрим, что нам предлагается:

rz@devbox:~/basics-graphics-music$ cd labs/01_and_or_not_xor_de_morgan 

rz@devbox:~/basics-graphics-music/labs/01_and_or_not_xor_de_morgan$ ls -l 

-rwxrwxr-x 1 rz rz  495 Jan  5 22:39 01_clean.bash 
-rwxrwxr-x 1 rz rz  495 Jan  5 22:39 02_simulate_rtl.bash 
-rwxrwxr-x 1 rz rz  495 Jan  5 22:39 03_synthesize_for_fpga.bash 
-rwxrwxr-x 1 rz rz  495 Jan  5 22:39 04_configure_fpga.bash 
-rwxrwxr-x 1 rz rz  495 Jan  5 22:39 05_run_gui_for_fpga_synthesis.bash 
-rwxrwxr-x 1 rz rz  495 Jan  5 22:39 06_choose_another_fpga_board.bash 
-rwxrwxr-x 1 rz rz  495 Jan  5 22:39 07_synthesize_for_asic.bash 
-rwxrwxr-x 1 rz rz  495 Jan  5 22:39 08_visualize_asic_synthesis_results_1.bash 
-rwxrwxr-x 1 rz rz  495 Jan  5 22:39 09_visualize_asic_synthesis_results_2.bash 
drwxrwxr-x 2 rz rz 4096 Jan 11 22:54 run 
-rw-rw-r-- 1 rz rz 1142 Jan  5 22:39 tb.sv 
-rw-rw-r-- 1 rz rz 2614 Jan  5 22:39 top.sv 

Каждый каталог с лабораторной работой содержит файлы: top.sv и tb.sv (расширение .sv означает текст на SystemVerilog), а также стандартный набор bash скриптов для выполнения операций синтеза (для ПЛИС и ASIC), запуска среды IDE и визуализации результатов. Далее я продемонстрирую только процесс синтеза и загрузки в ПЛИС. Но прежде давайте заглянем в top.sv.

rz@devbox:~/basics-graphics-music/labs/01_and_or_not_xor_de_morgan$ cat top.sv 

В самом начале файла мы видим описание интерфейса модуля top, который предоставляет нам окно во внешний мир — доступ к устройствам на плате, среди которых массив светодиодов, массив кнопок, порт UART и ряд других. Интерфейс модуля top общий для всех лабораторных работ в этом проекте, но отдельные примеры (лабы) могут использовать только часть предоставляемых возможностей.

`include "config.svh" 

module top 
# ( 
    parameter clk_mhz = 50, 
              w_key   = 4, 
              w_sw    = 8, 
              w_led   = 8, 
              w_digit = 8, 
              w_gpio  = 100 
) 
( 
    input                        clk, 
    input                        slow_clk, 
    input                        rst, 

    // Keys, switches, LEDs 

    input        [w_key   - 1:0] key, 
    input        [w_sw    - 1:0] sw, 
    output logic [w_led   - 1:0] led, 

    // A dynamic seven-segment display 

    output logic [          7:0] abcdefgh, 
    output logic [w_digit - 1:0] digit, 

    // VGA 

    output logic                 vsync, 
    output logic                 hsync, 
    output logic [          3:0] red, 
    output logic [          3:0] green, 
    output logic [          3:0] blue, 

    input                        uart_rx, 
    output                       uart_tx, 

    input                        mic_ready, 
    input        [         23:0] mic, 
    output       [         15:0] sound, 

    // General-purpose Input/Output 

    inout        [w_gpio  - 1:0] gpio 
); 

Далее следует установка значений по умолчанию для всех внешних сигналов, не используемых в данном примере:

 // assign led      = '0; 
       assign abcdefgh = '0; 
       assign digit    = '0; 
       assign vsync    = '0; 
       assign hsync    = '0; 
       assign red      = '0; 
       assign green    = '0; 
       assign blue     = '0; 
       assign sound    = '0; 
       assign uart_tx  = '1; 

Затем следует сам пример, разбираемый в рамках выполнения данной лабораторной работы. В данном случае нам демонстрируют, как описываются два входных однобитных сигнала a и b, которые привязываются к кнопкам key[] (многобитный сигнал) и однобитный сигнал результата result. Между сигналами a, b и result устанавливается однозначная логическая связь — логическая операция «Исключающее ИЛИ» (XOR). Выходной сигнал result привязывается к светодиоду led[0]. К светодиоду led[1] привязывается результат этой же логической операции, но иным способом — минуя промежуточные сигналы a и b.

    wire a = key [0]; 
    wire b = key [1]; 

    wire result = a ^ b; 

    assign led [0] = result; 

    assign led [1] = key [0] ^ key [1]; 

Далее следуют упражнения для самостоятельного выполнения, в данном случае на знание логических операций и правил де Моргана:

    // Exercise 1: Change the code below. 
    // Assign to led [2] the result of AND operation. 
    // 
    // If led [2] is not available on your board, 
    // comment out the code above and reuse led [0]. 

    // assign led [2] = 

    // Exercise 2: Change the code below. 
    // Assign to led [3] the result of XOR operation 
    // without using "^" operation. 
    // Use only operations "&", "|", "~" and parenthesis, "(" and ")". 

    // assign led [3] = 

    // Exercise 3: Create an illustration to De Morgan's laws: 
    // 
    // ~ (a & b) == ~ a | ~ b 
    // ~ (a | b) == ~ a & ~ b 

Попробуем синтезировать схему и загрузить её в ПЛИС. Запуск всего конвейера для получения битстрима осуществляется одной командой:

rz@devbox:~/basics-graphics-music/labs/01_and_or_not_xor_de_morgan$ bash 03_synthesize_for_fpga.bash 

Configuring for Lattice ECP5... 
OK 

Далее последует длинный, длинный лог результата работы всех утилит участвующих в процессе.

В самом конце, если все прошло удачно и без ошибок, мы увидим следующее:

Info: Program finished normally. 
ecppack --compress --freq 38.8 --input 01_and_or_not_xor_de_morgan_out.config --bit 01_and_or_not_xor_de_morgan.bin 
Would you like to upload to FPGA ? [y/N] 

Нас спрашивают, хотим ли мы загрузить полученный битстрим в ПЛИС прямо сейчас, на что я отвечаю N (нет). Скрипт заканчивает свою работу сообщив нам имя файла битстрима:

Resulting binary file is:
-rw-rw-r-- 1 rz rz 99064 Jan 12 23:49 /home/rz/basics-graphics-music/labs/01_and_or_not_xor_de_morgan/run/01_and_or_not_xor_de_morgan.bin 

7.3 Загрузка битстрима в микросхему ПЛИС

Настало время подключить нашу отладочную плату «Карно» к машине, на которой мы ведем разработку и запустить скрипт для прошивки:

rz@devbox:~/basics-graphics-music/labs/01_and_or_not_xor_de_morgan$ bash 04_configure_fpga.bash 
Configuring for Lattice ECP5... 
OK 
Would you like to upload to FPGA ? [y/N] y 

Скрипт спрашивает, действительно ли мы желаем выполнить загрузку в ПЛИС? Отвечаем y.

This board supports the following upload methods: 
[o] for openFPGALoader using JTAG based on FTDI FT2232 
[e] for ecpprog using JTAG based on FTDI FT2232 
Which upload method do you prefer ? O

Скрипт просит выбрать, каким программатором производить загрузку битстрима, openFPGALoader или ecpprog. Выбираем первый, т. е. вводим o.

Upload method for this board is: openloader 
Where to upload, SRAM or Flash ? [s/F] s 

Далее скрипт спрашивает, будем ли мы загружать битстрим в SRAM или в NOR Flash. В данном случае выбираем SRAM, т. е. вводим: s

Если плата подключена и USB программатор на ней детектировался системой, то мы получим следующий вывод от утилиты openFPGALoader:

make upload_openloader 
make[1]: Entering directory '/home/rz/basics-graphics-music/labs/01_and_or_not_xor_de_morgan/run' 
openFPGALoader -v --ftdi-channel 0  01_and_or_not_xor_de_morgan.bin 
No cable or board specified: using direct ft2232 interface 
Jtag frequency : requested 6.00MHz   -> real 6.00MHz   
found 1 devices 
index 0: 
	idcode 0x1111043 
	manufacturer lattice 
	family ECP5 
	model  LFE5UM-25 
	irlength 8 
File type : bin 
Open file: DONE 
Parse file: DONE 
bitstream header infos 
Part: LFE5U-25F-6CABGA256 
idcode: 41111043 
IDCode : 41111043 
displayReadReg 
	Config Target Selection : 0 
	Done Flag 
	Std PreAmble 
	No err 
Enable configuration: DONE 
SRAM erase: DONE 
Loading: [==================================================] 100.00% 
Done 
userCode: 00000000 
Disable configuration: DONE 
displayReadReg и
	Config Target Selection : 0 
	Done Flag 
	Std PreAmble 
	No err 

Сразу по окончанию работы утилиты openFPGALoader, микросхема ПЛИС на плате будет сконфигурирована и Ваш дизайн (лабораторная работа) активирован в ней.

Для того чтобы проверить, как работает дизайн, нажмем кнопки KEY0 и KEY1 сначала по отдельности, потом вместе. Светодиоды LED0 и LED1 должны реагировать соответственно дизайну.

Рис. 19. ПИР СЦХ-254 «Карно» в действии.
Рис. 19. ПИР СЦХ-254 «Карно» в действии.

Если Вы проводите синтез на удаленной системе (на сервере), к которой нет возможности подключить отладочную плату через USB, то загрузку битстрима можно осуществлять с любой локальной машины. Для этого на неё достаточно установить утилиту openFPGALoader. Как я уже упоминал выше, эта утилита доступна почти во всех дистрибутивах Linux и BSD, и установить её можно из системного репозитория, например командой:

$ sudo apt-get install openfpgaloader

После чего выполнять загрузку битстрима путем вызова утилиты openFPGALoader напрямую, предварительно скопировав бинарный файл с битстримом на локальную систему. Вызов утилиты осуществляется следующим образом:

# для загрузки в SRAM
$ openFPGALoader -v --ftdi-channel 0 01_and_or_not_xor_de_morgan.bin

# для загрузки в NOR flash
$ openFPGALoader -v -f --ftdi-channel 0 01_and_or_not_xor_de_morgan.bin

8. Типовой Makefile для синтеза с помощью Yosys

Для желающих попробовать Yosys я приведу небольшой Makefile с описанием последовательности (конвейера) запуска утилит для того, чтобы превратить цифровую схему из исходного описания на языке SystemVerilog в битстрим, готовый к заливке в микросхему ПЛИС. Данный файл предназначен для сборки под плату «Карно» с ПЛИС Lattice ECP5.

Итак, описываем проект: название, исходные файлы и зависимости:

NAME = 01_and_or_not_xor_de_morgan 
INC = /home/rz/basics-graphics-music/labs/common 
BOARD = karnix_ecp5_yosys 
READ_VERILOG += -p "read_verilog -sv /home/rz/basics-graphics-music/labs/01_and_or_not_xor_de_morgan/top.sv" 
DEPS += /home/rz/basics-graphics-music/labs/01_and_or_not_xor_de_morgan/top.sv 

Описываем тип корпуса микросхемы и файл распиновки внешних сигналов:

LPF = board_specific.lpf
DEVICE = 25k 
PACKAGE = CABGA256 

Запускаем по очереди: yosys — для синтеза, nextpnr-ecp5 — для размещения и трассировки, ecppack — для упаковки в битстрим:

all: $(NAME).bin

$(NAME).bin: $(PCF) $(DEPS) 
        yosys -p "verilog_defaults -add -I$(INC)" $(READ_VERILOG) -p "synth_ecp5 -json $(NAME).json -top board_specific_top" 
        nextpnr-ecp5 --package $(PACKAGE) --$(DEVICE) --json $(NAME).json --textcfg $(NAME)_out.config --lpf $(LPF) --lpf-allow-unconstrained 
        ecppack --compress --freq 38.8 --input $(NAME)_out.config --bit $(NAME).bin

Загрузка битстрима в ПЛИС:

upload_openloader: 
ifeq ("$(FLASH_METHOD)", "flash") 
        openFPGALoader -v --ftdi-channel $(FTDI_CHANNEL) -f --reset $(NAME).bin 
else 
        openFPGALoader -v --ftdi-channel $(FTDI_CHANNEL) $(NAME).bin 
endif 

Полный текст заготовок Makefile-а для своего проекта можно взять из проекта «basics-graphics-music» из следующих файлов:

Для ПЛИС Lattice ECP5: boards/karnix_ecp5_yosys/Makefile

Для ПЛИС Lattice iCE40: boards/ice40hx8k_evb_yosys/Makefile

Для ПЛИС Gowin GW1N: boards/tang_nano_9k_gowin_yosys/Makefile

Для ПЛИС Gowin GW2A: boards/tang_primer_20k_dock_gowin_yosys/Makefile

9. Файл описания внешних сигналов и ограничений (LPF, PCF, CST)

Каждый проприетарный тул имеет свой формат файла для описания внешних связей и типа внешних сигналов микросхемы ПЛИС. Так, в Gowin EDA для ПЛИС GW1N и GW2A используется файл формата .CST, в Lattice Diamond для ПЛИС iCE40 используется файл .PCF, а для ПЛИС ECP5 это файл .LPF.

В тулчейне Yosys файл с описанием внешних сигналов необходим утилите плэйсеру — NextPNR, для каждого вида микросхем ПЛИС существует своя версия этой утилиты: nextpnr-ice40, nextpnr-ecp5, nextpnr-gowin и т. д. Разработчики утилиты NextPNR не стали изобретать какой-то свой унифицированный формат файла, а сделали поддержку того формата, который принят для данной микросхемы ПЛИС. С одной стороны, это очень удобно при переносе проектов с проприетарных тулов. С другой, при переносе проекта с одной микросхемы ПЛИС на другую в рамках Yosys возникает необходимость заново создавать файл описания. На сколько это удобно — судите сами, но я бы предпочел, чтобы был еще один универсальный формат.

Разберем формат файла .LPF чуть более детально и посмотрим на некоторые из часто используемых директив для описания сигналов и ограничений в ПЛИС Lattice ECP5:

Конструкция LOCATE COMP "xxx" SITE "yyy"; привязывает внутренний сигнал xxx к выводу yyy микросхемы, например:

LOCATE COMP "CLK" SITE "B9"; // 25MHz crystal oscillator 
или
LOCATE COMP "GPIO[0]" SITE "B9"; // Connected to IO_6

Конструкция IOBUF PORT "xxx" IO_TYPE=LVCMOS33; указывает на то, что для сигнала xxx необходимо использовать аппаратных блок IOBUF - двунаправленный буфер (другие варианты не поддерживаются). Также эта директива указывает на то, что для данного сигнала нужно использовать буфер типа Low-voltage CMOS на 3.3V. Возможные варианты: LVCMOS33, LVCMOS25, LVCMOS18, LVCMOS15, LVCMOS12, LVCMOS33D, LVCMOS25D, LVCMOS15D, LVCMOS12D, LVCMOS18D .

Пример:

IOBUF PORT "CLK" IO_TYPE=LVCMOS33; 

Помимо IO_TYPE в ограничениях для данного вывода микросхемы можно указать необходимость в резисторе подтяжки. Делается это следующим образом:

PULLMODE=NONE
или
PULLMODE=UP
или
PULLMODE=DOWN

Также можно указать силу тока выходного каскада (драйвера) через директиву DRIVE, которая может принимать одно из значений: 4,6,8,10,12 и 16 которые соответствуют току в мА. Пример:

LOCATE COMP "io_gpio[0]" SITE "L1";           
IOBUF PORT "io_gpio[0]" IO_TYPE=LVCMOS33; 
IOBUF PORT "io_gpio[0]" PULLMODE=NONE DRIVE=16; 

Еще одна полезная директива FREQUENCY NET указывает на то, что данный сигнал используется для тактирования, следом указывается минимально разрешенная частота. Это подсказка плэйсеру, о том, что данный сигнал требуется трассировать особым образом и проверять задержки (тайминги) на возможность работы при заданной частоте. Пример:

FREQUENCY NET "clk" 25.00000 MHz;

Это пожалуй все, что необходимо знать начинающему про файл LPF. Более подробно со списком поддерживаемых директив можно ознакомиться в техническом описании на соответствующую микросхему ПЛИС.

Полный файл описания внешних сигналов и ограничений для различных ПЛИС и плат можно получить из проекта «basics-graphics-music»:

Для ПЛИС Lattice ECP5: boards/karnix_ecp5_yosys/board_specific.lpf

Для ПЛИС Lattice iCE40: boards/ice40hx8k_evb_yosys/board_specific.pcf

Для ПЛИС Gowin GW1N: boards/tang_nano_9k/board_specific.cst

Для ПЛИС Gowin GW2A: boards/tang_primer_20k_dock_gowin_yosys/board_specific.cst

10. Особенности синтаксиса Yosys

На момент написания этой статьи, из проекта «basics-graphics-music» тулчейном Yosys поддерживаются не все лабораторные работы. Это связано с тем, что синтаксис языка SystemVerilog весьма обилен и разнообразен и, как это часто бывает с любыми компиляторами, поддержка сложных синтаксических конструкций несколько отличается от тула к тулу.

Так получилось и с Yosys. Если попытаться синтезировать лабораторную работу 02_mux, то можно получить от утилиты yosys следующее сообщение об ошибке:



rz@devbox:~/basics-graphics-music/labs/01_and_or_not_xor_de_morgan$ cd ../02_mux/ 
rz@devbox:~/basics-graphics-music/labs/02_mux$

rz@devbox:~/basics-graphics-music/labs/02_mux$ bash 03_synthesize_for_fpga.bash 
Configuring for Lattice ECP5... 
OK 
...
Yosys 0.36+58 (git sha1 ea7818d31, gcc 7.5.0-3ubuntu1~18.04 -fPIC -Os) 
-- Running command `verilog_defaults -add -I/home/rz/basics-graphics-music/labs/common' -- 
-- Running command `read_verilog -sv /home/rz/basics-graphics-music/labs/02_mux/top.sv' -- 
1. Executing Verilog-2005 frontend: /home/rz/basics-graphics-music/labs/02_mux/top.sv 
Parsing SystemVerilog input from /home/rz/basics-graphics-music/labs/02_mux/top.sv' to AST representation. </font> <font size="1" style="font-size: 8pt">/home/rz/basics-graphics-music/labs/02_mux/top.sv:4: <span style="font-style: normal"><b>ERROR: Unimplemented compiler directive or undefined macro error_This_module_requires_support_for_multidimantional_arrays. 
Makefile:76: recipe for target '02_mux.bin' failed 
make: *** [02_mux.bin] Error 1 

Заглянув в файл top.sv в строках 3-5 мы увидим следующую директиву препроцессору заключенную в директиву ifdef:

`ifdef YOSYS 
    `error_This_module_requires_support_for_multidimantional_arrays 
`endif

Не сложно догадаться, что директивы с таким именем в Yosys нет и быть не может. Использование такой директивы это хак — способ остановить работу препроцессора Yosys с ошибкой и дать пользователю понять, что дальнейшая обработка его дизайна данным тулом невозможна, потому что Yosys, на момент написания этой статьи не поддерживал многомерные массивы.

Этот код условной компиляции (ifdef) с ошибкой был добавлен в код лабораторной работы мной, когда я занимался портированием проекта на тулчейн Yosys. Вообще в коде лабораторной работы 02_mux какой-то особой надобности в многомерных массивах нет, и её можно было бы смело переписать, используя только двумерные массивы (с ними проблем в Yosys нет), но после совещания с коллективом я пришел к выводу, что стоит оставить оригинальный код, так как студентам нужно продемонстрировать использование многомерных массивов сигналов в SystemVerilog.

Замечу, что 02_mux — это единственная лабораторная работа, которая не собирается (не синтезируется) тулчейном Yosys, код остальных лабораторных работ исправлен директивами условной «компиляции».

Отсутствие поддержки многомерных массивов — это не единственная проблема Yosys. Ниже я продемонстрирую еще несколько часто встречающихся конструкций языка SystemVerilog, которые не поддерживаются.

10.1 Макро YOSYS

Прежде всего надо сказать, что Yosys устанавливает макро YOSYS, что позволяет делать условную сборку проекта. Выглядит это совершенно обыденно:

`ifdef YOSYS
    /* some Yosys specific code */
`else
    /* code for other tools */
`endif

10.2 Многомерные массивы сигналов

Многомерные массивы сигналов (multi-dimentional arrays) поддерживаются только до двух уровней. Пример массива размером 1024 для ячеек памяти размерностью 32 бит, который может быть синтезирован Yosys-ом:

reg [1023:0] mem [31:0];

или

reg [1023:0][31:0] mem;

Описать память с банками уже не выйдет, т.е. вот такой синтаксис не сработает:

reg [3:0][1023:0][31:0] mem;

10.3 Функция возведения в степень ($POW) и оператор **

Отсутствует встроенная функция возведения в степень ($POW) и оператор **. Данный код не работает:

assign x = y ** 2;

Для второй степени это легко обходится следующим образом:

assign x = y * y;

Для других степеней придется писать свой код.

10.4 Функция не может иметь доступ к сигналу, описанному за её пределами

Функция (function) может оперировать только тем, что ей передано в параметрах. Доступ к сигналам и регистрам, которые находятся снаружи, изнутри функции запрещен.

Например, в лабе 13_music_recognition содержалась следующая последовательность функций, которая использовала сигнал distance, описанный выше в рамках модуля top. Это код Yosys-ом не синтезируется:

logic [19:0] distance;
//------------------------------------------------------------------------ 

function [19:0] check_freq_single_range (input [18:0] freq_100); 

   check_freq_single_range =    distance &gt; low_distance  (freq_100) 
                              &amp; distance &lt; high_distance (freq_100); 
endfunction </pre><p style="line-height: 100%">


//------------------------------------------------------------------------ 
function [19:0] check_freq (input [18:0] freq_100); 

   check_freq =   check_freq_single_range (freq_100 * 4) 
                | check_freq_single_range (freq_100 * 2) 
                | check_freq_single_range (freq_100    ); 

endfunction </pre>

Код был переписан таким образом, чтобы сигнал distance передавался в качестве одного из входных сигналов в функции его использующие:

    //------------------------------------------------------------------------ 

    function [19:0] check_freq_single_range (input [18:0] freq_100, input [19:0] distance); 

       check_freq_single_range =    distance > low_distance  (freq_100) 
                                  & distance < high_distance (freq_100); 
    endfunction 

    //------------------------------------------------------------------------ 

    function [19:0] check_freq (input [18:0] freq_100, input [19:0] distance); 

       check_freq =   check_freq_single_range (freq_100 * 4 , distance) 
                    | check_freq_single_range (freq_100 * 2 , distance) 
                    | check_freq_single_range (freq_100     , distance); 

    endfunction 

10.5 Сигналы нулевой или отрицательной размерности

Тут все просто, сигналы нулевой и, тем более, отрицательной размерности не поддерживаются и на попытку объявить такие сигналы Yosys выдает соответствующее сообщение об ошибке. Многие могут задаться вопросом, а зачем разработчику могут понадобиться такие бессмысленные сигналы? Ответ прост: такие сигналы обычно появляются в результате исполнения различных преобразований с использованием параметров конфигурации модулей.

Для иллюстрации возьмем модуль top любой из лабораторных работы. В определении этого модуля (см. 7.2) имеется ряд сигналов, размерность которых задана с изменяемым параметром, например:

input        [w_key   - 1:0] key,

где w_key — это параметр, определяющий число доступных на плате кнопок, передается он сюда сверху из board_specific_top.sv. Если на какой-либо из плат нет своих кнопок, то параметр w_key будет установлен в ноль и определение сигнала input выродится в что-то типа:

input        [0   - 1:0] key,

То есть будет попытка определить сигнал отрицательной размерности.

Такие тулы как Quartus и Vivado спокойно проглатывают такой синтаксис, а вот Yosys — нет.

10.6 Сложные битовые манипуляции иногда могут выдавать ошибку о loopback-ах

С этой ошибкой пока не удалось полностью разобраться. Ниже я приведу код из лабораторной работы 04_priority_encoder, который пришлось переработать:

`ifdef YOSYS
    wire [w - 1:0] c;
    genvar i;
    generate
        for (i = 0; i < w; i = i + 1) begin
            if (i == 0)
                assign c [0] = 1'b1;
            else
                assign c [i] = ~ in [i - 1] & c [i - 1];
        end
    endgenerate
`else
    wire [w - 1:0] c = { ~ in [w - 2:0] & c [w - 2:0], 1'b1 };
`endif

10.7 Глобализация сигналов

В Yosys поддерживаются следующие псевдомодули для "глобализации" сигналов, т. е. способ дать подсказку синтезатору и плэйсеру о том, что определенный сигнал требуется трассировать используя глобальные линии.

Для ПЛИС iCE40:

SB_GB clk_buf (.USER_SIGNAL_TO_GLOBAL_BUFFER(CLK), .GLOBAL_BUFFER_OUTPUT(slow_clk));

Для ПЛИС ECP5:

DCCA clk_buf (.CLKI(CLK), .CLKO(slow_clk), .CE(1));

Для ПЛИС Gowin:

BUFG clk_buf (.I(CLK), .O(slow_clk));

Для ПЛИС Gowin имеется следующий нюанс. Утилита плэйcер nextpnr-gowin, выполняющая размещение и трассировку для ПЛИС Gowin, не понимает эту фичу и отказывается выполнять работу. То есть этап на стадии работы утилиты yosys проходит и нетлист синтезируется успешно, но nextpnr-gowin данную фичу не поддерживает и в логе можно увидеть следующее информационное сообщение:

./gowin/arch.cc: log_info("BUFG isn't supported\n");

Однако, nextpnr-gowin все равно пытается разрулить искусственные тактовые сигналы оптимальным путем. Для примера, я завел счетчик-делитель и взял один из его битов в качестве slow_clk. Вот что я увидел в логе:

Info: Max frequency for clock 'div[22]': 315.86 MHz (PASS at 27.00 MHz)```

То есть nextpnr-gowin определил, что сигнал div[22] используется для тактирования и вывел его на глобальную линию, что видно по максимальной частоте!

На данный момент это всё, что мне известно об особенностях синтаксиса тулчейна Yosys. Если кто-то из читателей обладает дополнительной информацией — буду рад обсудить в комментариях и добавлю в текст статьи.

11. Анализируем сообщения от тулов

В ходе работы утилиты yosys и nextpnr выдают огромный поток информации о протекающем процессе, в котором, помимо сообщений о синтаксических ошибках, могут быть сообщения требующие внимания, особенно если ваш дизайн работает не так, как вы запланировали/предполагали. В этом случае следует внимательно изучить сообщения об оптимизации и сокращении цепей (nets), триггеров (DFFs — «D flip-flops») и модулей (modules). Возможно, что в вашем дизайне присутствует логическая ошибка, в результате которой часть «важных» сигналов становится логически бесполезными и оптимизатор их просто удалит за ненадобностью. Если такой сигнал/триггер обнаруживается среди сообщений перечисленных ниже, требуется тщательным образом проанализировать текст модулей с его участием и, возможно, переписать более прозрачно.

11.1 Сообщения от утилиты yosys

В выводе от утилиты yosys стоит обратить внимание на следующие сообщения.

1. На различные предупреждения:

Warning: Yosys has only limited support for tri-state logic at the moment. (/home/rz/basics-graphics-music/labs/common/tm1638_board.sv: 303)

Да, yosys поддерживает состояние высокого импеданса для сигнальных линий (tri-state или z), но эта поддержка имеет свои нюансы, о чем нам и сообщает синтезатор.

Или вот такое предупреждение:

Warning: Resizing cell port board_specific_top.slow_clk_buf.CE from 32 bits to 1 bits.

Здесь синтезатор сообщает нам, что объявленный сигнал slow_сlk_buf размерностью 32 бита был урезан до одного бита, так как этого вполне достаточно.

2. Сообщения об удаленных неиспользуемых модулях и неиспользуемых сигналах:

13.3.5. Analyzing design hierarchy.. 
Top module:  \board_specific_top 
...
Removing unused module `\seven_segment_display'. 
Removing unused module `\vga'. 
Removing unused module `\digilent_pmod_mic3_spi_receiver'. 
Removing unused module `\shift_reg'. 
Removing unused module `\slow_clk_gen'. 
Removing unused module `\inmp441_mic_i2s_receiver'. 
Removing unused module `\strobe_gen'. 
Removing unused module `\counter_with_enable'. 
Removing unused module `\i2s_audio_out'. 
Removing unused module `\tm1638_sio'. 
Removing unused module `\tm1638_board_controller'. 
Removing unused module `\top'. 
Removed 12 unused modules. 

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

3. Сообщения об удаленных неиспользуемых процессах:

13.4.11. Executing PROC_CLEAN pass (remove empty switches from decision trees). 
Removing empty process `TRELLIS_FF.$proc$/usr/local/bin/../share/yosys/ecp5/cells_sim.v:0$403'. 
Found and cleaned up 2 empty switches in `\TRELLIS_FF.$proc$/usr/local/bin/../share/yosys/ecp5/cells_sim.v:350$402'. 
Removing empty process `TRELLIS_FF.$proc$/usr/local/bin/../share/yosys/ecp5/cells_sim.v:350$402'. 
Removing empty process `DPR16X4C.$proc$/usr/local/bin/../share/yosys/ecp5/cells_sim.v:0$377'. 
Found and cleaned up 1 empty switch in `\DPR16X4C.$proc$/usr/local/bin/../share/yosys/ecp5/cells_sim.v:285$354'. 
Removing empty process `TRELLIS_DPR16X4.$proc$/usr/local/bin/../share/yosys/ecp5/cells_sim.v:0$320'. 
Found and cleaned up 1 empty switch in `\TRELLIS_DPR16X4.$proc$/usr/local/bin/../share/yosys/ecp5/cells_sim.v:221$296'. 
Removing empty process `TRELLIS_DPR16X4.$proc$/usr/local/bin/../share/yosys/ecp5/cells_sim.v:213$295'. 
Found and cleaned up 1 empty switch in `$paramod$c57045fef6efabbb852bcb6522a23329a7270ab8\slow_clk_gen.$proc$/home/rz/basics-graphics-music/labs/common/slow_clk_gen.sv:35$406'. 
Removing empty process `$paramod$c57045fef6efabbb852bcb6522a23329a7270ab8\slow_clk_gen.$proc$/home/rz/basics-graphics-music/labs/common/slow_clk_gen.sv:35$406'. 
Cleaned up 5 empty switches. 

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

Последнее сообщение относится к «медленному клоку», который формируется в модуле-обертке, но не задействован в модуле данной лабораторной работы, поэтому тоже будет удален.

4. Сообщения о сокращении размерности сигналов:

13.14. Executing WREDUCE pass (reducing word size of cells). 
Removed top 2 bits (of 4) from port Y of cell board_specific_top.$not$/home/rz/basics-graphics-music/boards/karnix_ecp5_yosys/board_specific_top.sv:91$4 ($not). 
Removed top 2 bits (of 4) from port A of cell board_specific_top.$not$/home/rz/basics-graphics-music/boards/karnix_ecp5_yosys/board_specific_top.sv:91$4 ($not). 
Removed top 2 bits (of 4) from wire board_specific_top.top_key. 
Removed top 2 bits (of 4) from wire board_specific_top.top_led. 

Синтезатор анализирует, как используются биты в многобитных сигналах и если они не используются, то синтезатор отправит их под сокращение, то есть урежет размерность.

5. Статистика по утилизации ресурсов

13.47. Printing statistics. 
=== board_specific_top === 
Number of wires:                 33 
Number of wire bits:            125 
Number of public wires:          33 
Number of public wire bits:     125 
Number of memories:               0 
Number of memory bits:            0 
Number of processes:              0 
Number of cells:                  1 
  LUT4                            1 

Статистика тоже имеет важное и полезное значение. Разработчик всегда должен иметь представление о том, какое количество сигналов, триггеров, процессов или ячеек памяти в его дизайне и, если это теоретическое значение начинает расходится с тем, что yosys выдает в результате синтеза, это явный признак логической ошибки, либо неверного использования языковых конструкций языка Verilog.

В приведенном выше сообщении об используемых ресурсах для лабораторной работы 01_and_or_not_xor_de_morgan видно, что весь дизайн лабораторной работы уложился в одну макроячейку (cell) из одного LUT-4, так как в данной лабораторной работе была реально задействована только комбинационная логика из нескольких логических элементов, что покрывается возможностями одного LUT-4.

А сейчас проведем следующий эксперимент: добавим в самый конец модуля этой же лабораторной работы код для регистра-счетчика counter размерностью 8 бит, будем увеличивать его каждый раз когда нажата клавиша KEY[2], а старший бит counter[7] выведем на LED[2], т. е. добавим следующий код:

    logic [7:0] counter; 

    assign led[2] = counter[7]; 
 
    always_ff @ (posedge clk or posedge rst) 
            if(rst) 
                    counter <= 0; 
            else if(key[2]) 
                    counter <= counter + 8'd1; 

Запустим синтез, дождемся его завершения (а он обязан завершиться без ошибки), загрузим в ПЛИС и протестируем. Как бы долго мы не удерживали клавишу KEY[2], состояние светодиода LED[2] меняться не будет, что очень странно, ведь синтез завершился без ошибок! Для решения этой проблемы сначала посмотрим на статистику по задействованным ресурсам, выдаваемую утилитой yosys, здесь мы увидим следующее:

13.47. Printing statistics. 
=== board_specific_top === 
Number of wires:                 34 
Number of wire bits:            133 
Number of public wires:          34 
Number of public wire bits:     133 
Number of memories:               0 
Number of memory bits:            0 
Number of processes:              0 
Number of cells:                  1 
  LUT4                            1 

Мы обнаружим, что наш измененный дизайн всё еще укладывается в один LUT-4 и в нём нет никаких триггеров, а такого быть не должно. Но как так получилось?! Анализируем вывод yosys более детально и обнаруживаем следующее предупреждение:

13.9. Executing OPT_CLEAN pass (remove unused cells and wires). 
Finding unused cells or wires in module \board_specific_top.. 
Warning: Driver-driver conflict for \i_top.counter [7] between cell $flatten\i_top.$procdff$454.Q and constant 1'0 in board_specific_to p: Resolved using constant. 
Removed 12 unused cells and 35 unused wires. 

Здесь yosys сообщает нам, что он увидел конфликт между двумя источниками — сигналом i_top.counter [7] и константой 1'0 которые пытаются задавать («драйвить») один и тот же сигнал и в этом конфликте константа выиграла, а значит всё, что далее связано с сигналом i_top.counter[7] было по цепочек удалено за ненадобностью!

Откроем файл с кодом модуля, внимательно просмотрим его с самого начала и обнаружим вот такой кусочек кода:

    `ifndef VERILATOR 

    generate 
        if (w_led > 2) 
        begin : unused_led 
            assign led [w_led - 1:2] = '0; 
        end 
    endgenerate 

    `endif 

Но что делает этот код и зачем он тут? Если вкратце, то этот код «приземляет» неиспользуемые линии led, если модуль синтезируется, а не симулируется, точнее он присваивает неиспользуемым ранее битам сигнала led значение 0. Этот код добавлен другим разработчиком и мы, не обратив на него внимания и добавив свой код в конец модуля, получили нерабочий модуль, который успешно синтезируется без ошибок!

Ну что же, исправим это дело — временно изменим условие ifdef так, чтобы этот обнуляющий код не подключался и пересоберем модуль еще раз. В результате мы получим следующую статистику от утилиты yosys:

13.47. Printing statistics. 
=== board_specific_top ===
Number of wires:                 49 
Number of wire bits:            159 
Number of public wires:          49 
Number of public wire bits:     159 
Number of memories:               0 
Number of memory bits:            0 
Number of processes:              0 
Number of cells:                 15 
  CCU2C                           4 
  LUT4                            3 
  TRELLIS_FF                      8 

Хо-хо! Теперь мы видим, что наш дизайн синтезировался в схему, содержащую восемь триггеров (блоков TRELLIS_FF), что соответствует нашему регистру-счетчику, и четыре блока CCU2C, из которых построен сумматор. Загрузим полученный битстрим в ПЛИС и убедимся, что индикатор LED2 реагирует на нажатия кнопки KEY2.

Примерно таким образом приходится действовать разработчику при поиске логических засад в дизайне, над которым он работает, и в этом деле yosys старается помочь в меру своих возможностей.

11.2 Сообщения от утилиты nextpnr

Теперь разберем некоторые важные сообщения от утилиты nextpnr. Напомню, что утилита nextpnr (для ПЛИС ECP5 она называется nextpnr-ecp5) производит размещение блоков, полученных в результате синтеза, внутри микросхемы ПЛИС и трассировку сигнальных цепей между ними. В процессе своей работы утилита nextpnr производит ряд оптимизаций в поиске оптимального расположения с минимизацией объема задействованных ресурсов микросхемы ПЛИС, а также производит привязку линий ввода вывода к внешним выводам микросхемы. В процессе оптимизации эта утилита выполняет статический анализ задержек (STA), вычисляет критические (самые «длинные») пути в схеме и рассчитывает максимально допустимые частоты, исходя из свойств блоков выбранной микросхемы ПЛИС и полученной топологии размещения.

Прежде чем мы приступим, давайте ознакомимся с терминологией, используемой в утилите nextpnr. Ниже приведена небольшая выдержка из NextPnR FAQ:

Терминология базы данных дизайна:

  • Cell (ячейка): Логическая сущность или физический блок внутри нетлиста. Упаковщик, являющийся частью NextPnR, комбинирует или иным способом модифицирует ячейки, а плэйсер размещает их на доступных Базовых Элементах (Bels).

  • Port (порт): Вход или выход ячейки (cell), может быть связан только с одной цепью (net).

  • Net (цепь): Связь между портами в нетлисте, прямой аналог электрической цепи. Цепь может быть разведена с помощью одного или нескольких проводников (wires) внутри микросхемы. Цепи всегда имеют размерность в один двоичный сигнал (один бит). Многобитные цепи всегда разбиваются на составные части.

  • Source (источник): Выходной порт ячейки (cell), управляющий заданной цепью (net).

  • Sink (сток): Входной порт ячейки (cell), управляемый заданной цепью (net).

  • Arc (дуга): Пара источник-приемник.

Терминология архитектурной базы данных:

  • Bel («базэл»): Базовый Элемент — функциональный блок микросхемы ПЛИС, такой как: «логическая ячейка» (LC), ячейка ввода/вывода (IO cell), блочная память (block RAM) и прочие аппаратные блоки ПЛИС. На каждый «базэл» может быть размещена только одна ячейка (cell).

  • Pin (вывод): Внешний «пин» ввода/вывод «базэла» (Bel), постоянно соединяется одним проводником внутри ПЛИС.

  • Pip («ПТС»): Программируемая точка связи, соединяющая несколько проводников (wires) в заданном направлении.

  • Wire (проводник): Фиксированное физическое соединение внутри микросхемы ПЛИС, между ПТС и/или выводами «базэлов».

  • Alias («алиас»): Специальная ПТС, представляющая постоянное (неизменяемое) соединение между двумя проводниками.

  • Group (группа): Может содержать все выше приведенные сущности: bels, pips, wires и другие группы.

  • BelBucket: Группа «базэлов» образующая «покрытие множества» (см. «Задачу о покрытии множества»).

Терминология процессов:

  • Packing (упаковка): Процесс группирования ячеек, полученных от синтезатора, в более крупные «логические ячейки».

  • Placing (размещение): Процесс поиска и размещения упакованных ячеек в базовые элементы (базэлы).

  • Routing (трассировка): Процесс поиска и объединения цепей проводниками.

Еще термины:

  • Binding (привязка): Привязка цепей к проводникам и ячеек к базовым элементам.

  • Path (путь): Все дуги, соединяющие вывод одного FF триггера («флип-флопа») с основным входом другого FF триггера.

Давайте посмотрим, какую же полезную информацию мы можем добыть из вывода nextpnr. Сразу после запуска утилита выводит информационное сообщение о том, сколько и каких ресурсов присутствует в топологии, которая поступила ей на вход от синтезатора:

Info: Logic utilisation before packing: 
Info:     Total LUT4s:        11/24288     0% 
Info:         logic LUTs:      3/24288     0% 
Info:         carry LUTs:      8/24288     0% 
Info:           RAM LUTs:      0/ 3036     0% 
Info:          RAMW LUTs:      0/ 6072     0% 
Info:      Total DFFs:         8/24288     0% 

Первое число в третьей графе — это количество задействованных ресурсов каждого вида. Второе число — это максимально доступное количество ресурса для выбранного типа и корпуса микросхемы ПЛИС. Как я уже упоминал, на плате «Карно» установлена микросхема ПЛИС ECP5 c 25K логических ячеек. Обозначение «25K» это чисто маркетинговая уловка, на самом деле логических ячеек в данной микросхеме немного меньше, а именно - 24288 штук. Как видим, не только ребята из Юго-Восточной Азии любят приукрасить действительность.

Далее мы видим привязку сигнальных линий к выводам («ногам») микросхемы:

Info: Packing IOs.. 
Info: pin 'VGA_VS$tr_io' constrained to Bel 'X0/Y44/PIOD'. 
Info: pin 'VGA_R[3]$tr_io' constrained to Bel 'X72/Y8/PIOA'. 
...
Info: pin 'UART_TX$tr_io' constrained to Bel 'X58/Y0/PIOB'. 
Info: pin 'UART_RX$tr_io' constrained to Bel 'X58/Y0/PIOA'. 
Info: pin 'SW[1]$tr_io' constrained to Bel 'X29/Y0/PIOB'. 
Info: pin 'SW[0]$tr_io' constrained to Bel 'X29/Y0/PIOA'. 
Info: pin 'LED[3]$tr_io' constrained to Bel 'X67/Y0/PIOA'. 
Info: pin 'LED[2]$tr_io' constrained to Bel 'X67/Y0/PIOB'. 
...
Info: pin 'KEY[3]$tr_io' constrained to Bel 'X62/Y0/PIOB'. 
Info: pin 'KEY[2]$tr_io' constrained to Bel 'X62/Y0/PIOA'. 
..
Info: pin 'GPIO[11]$tr_io' constrained to Bel 'X0/Y32/PIOD'. 
Info: pin 'GPIO[10]$tr_io' constrained to Bel 'X0/Y32/PIOC'. 
...

Иногда можно увидеть следующее предупреждающее сообщение:

Warning: IO 'some_output' is unconstrained in LPF and will be automatically placed

Таким способом nextpnr сообщает разработчику о том, что в его дизайне, в модуле top, присутствует внешний сигнал some_output, который не описан в LPF, а следовательно, плэйсер не знает, как его привязать к внешнему выводу микросхемы, и этот сигнал будет привязан автоматически к первому подходящему (случайному) выводу. На такие сообщения нужно обращать внимание и не допускать присутствие в схеме внешних сигналов, не привязанных к физическим выводам. Несоблюдение этого правила может приводить к серьезным утечкам тока (если этот случайный вывод оказался на «земле»), как следствие - нестабильная работа микросхемы ПЛИС и выход её из строя!

После выполнения упаковки, утилита nextpnr может выдать сообщение о выполненной привязке сигналов к глобальным линиям:

Info: Promoting globals... 
Info:     promoting clock net CLK$TRELLIS_IO_IN to global network

Здесь утилита сообщает о том, что она посчитала, что сигнал CLK является тактовой цепью и он будет трассирован с использованием глобальной цепи. Не всегда nextpnr распознает тактовые цепи и цепи сброса корректно, поэтому нужно следить за тем, как размещаются такие сигналы и предпринимать определенные действия — имеется способ маркировать такие сигналы, см. главу «10.7 Глобализация сигналов». Иногда бывает так, что nextpnr совершенно напрасно считает какой-нибудь из промежуточных сигналов тактовым и тоже размещает его в глобальных линиях, понапрасну тратя ограниченный ресурс. Способа избавиться от такого эффекта я пока не нашел, но заметил, что в имени сигнала лучше не использовать строки «clk» и «CLK».

Далее nextpnr еще раз выдает статистику о результатах упаковки и размещения, но в этот раз в статистике уже присутствуют наименования аппаратных блоков конкретной микросхемы, которые требуется задействовать для размещения схемы:

Info: Device utilisation: 
Info:             TRELLIS_IO:    39/  197    19% 
Info:                   DCCA:     1/   56     1% 
Info:                 DP16KD:     0/   56     0% 
Info:             MULT18X18D:     0/   28     0% 
Info:                 ALU54B:     0/   14     0% 
Info:                EHXPLLL:     0/    2     0% 
Info:                EXTREFB:     0/    1     0% 
Info:                   DCUA:     0/    1     0% 
Info:              PCSCLKDIV:     0/    2     0% 
Info:                IOLOGIC:     0/  128     0% 
Info:               SIOLOGIC:     0/   69     0% 
Info:                    GSR:     0/    1     0% 
Info:                  JTAGG:     0/    1     0% 
Info:                   OSCG:     0/    1     0% 
Info:                  SEDGA:     0/    1     0% 
Info:                    DTR:     0/    1     0% 
Info:                USRMCLK:     0/    1     0% 
Info:                CLKDIVF:     0/    4     0% 
Info:              ECLKSYNCB:     0/   10     0% 
Info:                DLLDELD:     0/    8     0% 
Info:                 DDRDLL:     0/    4     0% 
Info:                DQSBUFM:     0/    8     0% 
Info:        TRELLIS_ECLKBUF:     0/    8     0% 
Info:           ECLKBRIDGECS:     0/    2     0% 
Info:                   DCSC:     0/    2     0% 
Info:             TRELLIS_FF:     8/24288     0% 
Info:           TRELLIS_COMB:    17/24288     0% 
Info:           TRELLIS_RAMW:     0/ 3036     0% 

Данный результат является промежуточным, он подается на вход оптимизатора и наступает самое интересное — процесс «утрясывания» и «перетасовывания» схемы, удаления из неё лишних частей и сокращение длинных связей. Этот процесс стохастический, т. е. в нем присутствует элемент случайности.

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

Info: Max frequency for clock '$glbnet$CLK$TRELLIS_IO_IN': 457.04 MHz (PASS at 12.00 MHz)

В данном случае нам сообщают, что максимально возможная частота тактового сигнала составит 457,02 МГц и она превышает (pass) указанный в ограничениях лимит в 12,00 МГц. Иными словами, все ок и беспокоиться не о чем. Но часто бывает и наоборот — рассчитываемая максимально допустима частота становится ниже заданной для данного дизайна и тогда придется либо перерабатывать дизайн и понижать требования к частотам, либо… дождаться окончания трассировки. Дело в том, что на этапе трассировки тоже работает оптимизация и в некоторых случаях трассировщик может еще немного уменьшить задержки.

Аналогично тактовым сигналам производятся расчеты «критического пути» для остальных сигналов в схеме и вычисление задержки для них. В конце концов, после выполнения трассировки, утилита выдаст статистику по имеющимся критическим путям:

Info: Max frequency for clock '$glbnet$CLK$TRELLIS_IO_IN': 371.20 MHz (PASS at 12.00 MHz) 
Info: Max delay <async>                           -> <async>                          : 2.86 ns 
Info: Max delay <async>                           -> posedge CLKInfo: Max delay posedge $glbnet" class="formula inline">CLK$TRELLIS_IO_IN -> <async>                          : 1.75 ns 

Здесь мы видим, что в процессе трассировки максимально возможная частота для сигнала CLK уменьшилась с 457,04 МГц до 371,20 МГц, но все еще проходит заданное ограничение в 12,0 МГц. На этом этапе непрохождение заданного лимита по частоте является фатальным и если это случается, то утилита nextpnr останавливается с ошибкой.

Успешное завершение утилиты nextnpr сигнализируется сообщением:

Info: Program finished normally.

Для наглядности, ниже я приведу вывод сообщений для одного из моих дизайнов, в котором как раз присутствует ситуация, когда рассчитываемая максимальная частота в критических путях не проходит заданное ограничение (ниже лимита) и расскажу, как я эту проблему решил.

Мой дизайн VexRiscvWithHUB12ForKarnix (см. главу 18) использует синтезируемое ядро VexRiscv, синтезируемый Ethernet контроллер и еще некоторое количество синтезируемой логики для организации видео фреймбуфера для работы со светодиодными матрицами. Для выполнения задачи мне требовалось, чтобы вычислительное ядро работало на частоте не менее 60 МГц. Для этого был задействован блок PLL на вход которого подается частота 25 МГц от генератора, распаянного на плате, преобразуется в 60 МГц и подается на вычислительное ядро.

После окончания первой стадии (размещения), утилита nextpnr выдала следующую статистику по критическим путям:

Info: Max frequency for clock '$glbnet$io_lan_rxclk$TRELLIS_IO_IN': 62.89 MHz (PASS at 25.00 MHz) 
Info: Max frequency for clock '$glbnet$io_lan_txclk$TRELLIS_IO_IN': 65.24 MHz (PASS at 25.00 MHz) 
Info: Max frequency for clock             '$glbnet$core_pll_CLKOP': 38.30 MHz (FAIL at 60.00 MHz) 

Info: Max delay <async>                                    -> posedge $glbnet$core_pll_CLKOP            : 9.00 ns 
Info: Max delay <async>                                    -> posedge $glbnet$io_lan_rxclk$TRELLIS_IO_IN: 2.88 ns 
Info: Max delay posedge $glbnet$core_pll_CLKOP             -> <async>                                   : 18.67 ns 
Info: Max delay posedge $glbnet$core_pll_CLKOP             -> posedge $glbnet$io_lan_rxclk$TRELLIS_IO_IN: 5.51 ns 
Info: Max delay posedge $glbnet$core_pll_CLKOP             -> posedge $glbnet$io_lan_txclk$TRELLIS_IO_IN: 4.07 ns 
Info: Max delay posedge $glbnet$io_lan_rxclk$TRELLIS_IO_IN -> posedge $glbnet$core_pll_CLKOP            : 1.84 ns 
Info: Max delay posedge $glbnet$io_lan_txclk$TRELLIS_IO_IN -> <async>                                   : 4.24 ns 
Info: Max delay posedge $glbnet$io_lan_txclk$TRELLIS_IO_IN -> posedge $glbnet$core_pll_CLKOP : 1.60 ns 

из которой видно, что расчет для сигнала core_clk_CLKOP дает максимальную частоту 38.30 МГц.

Дождавшись окончания трассировки, я получил следующую статистику по критическим путям:

Info: Max frequency for clock '$glbnet$io_lan_rxclk$TRELLIS_IO_IN': 88.19 MHz (PASS at 25.00 MHz) 
Info: Max frequency for clock '$glbnet$io_lan_txclk$TRELLIS_IO_IN': 74.08 MHz (PASS at 25.00 MHz) 
ERROR: Max frequency for clock             '$glbnet$core_pll_CLKOP': 56.58 MHz (FAIL at 60.00 MHz) 
…
0 warnings, 1 error

Видно, что оптимизатор в процессе трассировки хорошо поработал и ужал задержку так, что максимально возможная частота для сигнала core_pll_CLKOP поднялась аж до 56,58 МГц, но всё еще не дотягивает до 60,0 МГц.

К сожалению, в моём случае перерабатывать дизайн было крайне нежелательно, и я решил поиграться с параметром --seed утилиты nextpnr-ecp5 (задается в Makefile). Это параметр инициализации генератора псевдослучайных чисел, используемых для первоначального случайного размещения. Я заметил, что изменяя этот параметр можно в некоторых пределах варьировать результаты размещения и трассировки — для них получаются немного отличающиеся задержки в критических путях. Таким образом, где-то на 15-й попытке мне удалось подобрать такое число и получить такое размещение, что максимальная частота для сигнала core_pll_CLKOP перевалила за 60,0 МГц и я получил рабочий битстрим. Ниже вывод статистики от nextpnr:

Info: Max frequency for clock '$glbnet$io_lan_rxclk$TRELLIS_IO_IN': 82.36 MHz (PASS at 25.00 MHz) 
Info: Max frequency for clock '$glbnet$io_lan_txclk$TRELLIS_IO_IN': 67.20 MHz (PASS at 25.00 MHz) 
Info: Max frequency for clock             '$glbnet$core_pll_CLKOP': 61.35 MHz (PASS at 60.00 MHz) 

Еще один способ решить эту же проблему — это увеличить параметр --speed который задает «грейд» (качество исполнения) микросхемы ПЛИС. На плате «Карно» использована микросхема 6-го грейда, но увеличив speed до 7-го я получил вполне рабочий битстрим, который был успешно проверен рядом тестов. Да, такой способ более опасный, может подвести в самый не подходящий момент, я рекомендую прибегать к нему только в случаях отладки и проведения экспериментов, но не для конечного изделия.

12. Синтезируемая ЭВМ

Идея собрать вычислительную машину полностью из микросхем программируемой логики пришла людям в головы сразу, как только появились достаточно объемные по ресурсам микросхемы ПЛУ и ПЛИС. На сайте Wikipedia на страничке посвященной ПЛИС есть упоминание о том, что идея разработки первой «синтезируемой» ЭВМ была предложена неким Стивом Кассельманом в 1987 году и, при финансовой поддержке центра управления ВМФ США (Naval Surface Warfare Department), такая машина была успешно создана и запатентована в 1992 году. Машина содержала 600 000 программируемых вентилей (т. е. логических элементов, не макроячеек и не LUT-ов), следовательно логично предположить, что эта машина была построена на микросхемах PAL и PLD. К сожалению, какой-либо дополнительной информации об этом изобретении мне найти не удалось.

Тем не менее, это событие тут же подстегнуло развитие совершенно нового направления в цифровой электронике — создание синтезируемых микропроцессорных ядер (или «soft-core»). Так, в 1990-х годах все именитые производители ПЛИС отметились выпуском своих 8-ми битных синтезируемых ядер: PicoBlaze от Xilinx, Nios от Altera, LatticeMicro8 от Lattice Semi. Все эти синтезируемые ядра были построены по RISC архитектуре и требовали совсем небольшое (по современным меркам) количество ресурсов. Для примера, ядро LatticeMicro8 требовало всего 200 LUT-ов. В начале 2000-х годов все они получили апгрейд: MicroBlaze — 32/64 бит RISC, Nios II - 32 бит RISC и LatticeMicro32 — тоже 32 бит RISC.

Первоначально синтезируемые микропроцессорные ядра распространялись как отдельно лицензируемые коммерческие продукты (IP-блоки) в составе фирменного IDE, но к концу 2000-х годов все они либо были опубликованы под свободными лицензиями, либо получили опенсорсные клоны и аналоги. Среди чисто опенсорсных ядер стоит упомянуть OpenRISC (or1k) — проект Дамьяна Лампрета (Damjan Lampret), основанного в далеком 1999 году, одного из ранних предшественников и конкурентов RISC-V. OpenRISC поддерживается коммерческой организацией OpenCores, но проповедует принципы «open-source hardware» (OSHW) и имеет определенную популярность по сей день, не смотря на то, что многие его сторонники перешли на сторону RISC-V.

В те же 2000-е годы для всех известных синтезируемых ядер был портирован GNU C Compiler Toolchain, что широко распахнуло дверь в мир синтезируемых микропроцессоров перед энтузиастами, любителями и профессиональными программистами. Примерно в это же время для архитектур синтезируемых ядер были портированы различные операционные системы — FreeRTOS, Linux и ряд других.

Что же из себя представляет синтезируемый микропроцессор? Как несложно догадаться, это текст на языке HDL, чаще на Verilog, реже на VHDL, но встречается и такое, что разные микроархитектурные части одного ядра могут реализовываться на различных языках, и даже не на Verilog или VHDL, а на специально разработанном языке.

Хорошим примером синтезируемого опенсорсного микропроцессора может служить VexRiscv — синтезируемое ядро, написанное на специальном HDL языке под названием SpinalHDL. Разработчиком софт-ядра VexRiscv и языка SpinalHDL является швейцарец Шарль Папон. Свою разработку он представил на конкурс RISC-V SoftCPU Contest проводимого RISC-V Foundation в 2018 году, где занял первое место и получил приз в $6000 USD за достижение максимальной производительности на ПЛИС Lattice и Microsemi. Далее мы попытаемся разобраться, в чем особенности VexRiscv и зачем автору потребовалось изобретать свой собственный язык описания аппаратуры для создания софт-ядра.

12.1 Синтезируемое вычислительное ядро VexRiscv

VexRiscv — это полностью open source реализация вычислительно ядра архитектуры RISC-V. Это очень гибко конфигурируемое ядро, его можно ужать до применения на очень ограниченном ресурсе и программировать только на ассемблере для «голого железа», а можно легко расширить, включив поддержку кэширования инструкций и данных, поддержку MMU, FPU, атомарных инструкций и соорудить на нем многопроцессорную систему для запуска ОС Linux прямо внутри ПЛИС. Шарль Папон, разработчик VexRiscv, вдохновленный достижениями быстроразвивающейся софтверной индустрии, перенес и применил некоторые её принципы на разработку цифровой аппаратуры, а именно — идею объектно-ориентированных плагинов (plugin). В рамках VexRiscv все его компоненты — это плагины, которые подключаются к разным стадиям конвейера. Плагины экспортируют свои структуры данных (сигнальные линии), которые автоматически становятся доступны во всех местах конвейера. Плагины могут предоставлять различные сервисы (service) другим плагинам. Плагины можно заменять и расширять. При таком подходе у автора получился гибко настраиваемый микропроцессор, в котором всё, вплоть от счетчика команд (PC) и регистрового файла до менеджера обработки «опасностей» (hazards), может быть заменено, расширено и углублено. Но самое интересное, что в погоне за гибкостью и функциональностью автору удалось не потерять в производительности, за что он и бы удостоен приза.

Ниже перечислены некоторые особенности VexRiscv, взятые со страницы README проекта:

  1. Ядро VexRiscv поддерживает инструкции следующих расширений спецификации RISC-V:

  • RV32I — стандартный минимальный набор с целочисленными операциями;

  • [M] — целочисленные умножения и деления;

  • [A] — атомарное исполнение операций изменения ячеек памяти;

  • [F] — операции с плавающей запятой (floating-point);

  • [D] — операции с подвижной запятой двойной точности (double-precision floating-point);

  • [C] — сжатые инструкции (compressed instruction set), позволяют существенно уменьшить объем программного кода.

  1. Ядро VexRiscv может иметь конвейер от двух до пяти ступеней: [Fetch], Decode, Execute, [Memory], [WriteBack] — ступени, обозначенные в квадратных скобках, могут быть отключены.

  2. Замеры производительности ядра показывают от 1.44 DMIPS/MHz до 1.57 DMIPS/MHz, в зависимости от набора включенных фич.

  3. VexRiscv оптимизирован для синтеза в ПЛИС и при этом не использует ни единого вендорозависимого IP-блока, т. е. является самодостаточным и легко переносимым.

  4. VexRiscv поддерживает шины AXI, Avalon и Wishbone, что позволяет легко строить на базе этого ядра сложные многопроцессорные системы-на-кристалле.

  5. Содержит опциональный блок MUL/DIV.

  6. Опциональный блок 32-х и 64-х битного FPU.

  7. Опциональный и настраиваемый кеш данных (D$) и инструкций (I$).

  8. Опциональный MMU для виртуализации памяти.

  9. Опциональная поддержка JTAG, что позволяет вести отладку через OpenOCD и GDB.

  10. Опциональная поддержка прерываний и исключений согласно RISC-V Privileged ISA Specification v1.10.

  11. Две реализации инструкции сдвига: полный однотактовый «full barrel shifter» и многотактовый «shiftNumber».

Результаты синтеза и замеры производительности разных конфигураций ядра VexRiscv:

VexRiscv small (RV32I, 0.52 DMIPS/Mhz, no datapath bypass) ->
    Artix 7     -> 240 Mhz 556 LUT 566 FF 
    Cyclone V   -> 194 Mhz 394 ALMs
    Cyclone IV  -> 174 Mhz 831 LUT 555 FF 
    iCE40       -> 85 Mhz 1292 LC
VexRiscv small and productive with I$ (RV32I, 0.70 DMIPS/Mhz, 4KB-I$)  ->
    Artix 7     -> 220 Mhz 730 LUT 570 FF 
    Cyclone V   -> 142 Mhz 501 ALMs
    Cyclone IV  -> 150 Mhz 1,139 LUT 536 FF 
    iCE40       -> 66 Mhz 1680 LC
VexRiscv linux balanced (RV32IMA, 1.21 DMIPS/Mhz 2.27 Coremark/Mhz, with cache trashing, 4KB-I$, 4KB-D$, single cycle barrel shifter, catch exceptions, static branch, MMU, Supervisor, Compatible with mainstream linux) ->
    Artix 7     -> 180 Mhz 2883 LUT 2130 FF 
    Cyclone V   -> 131 Mhz 1,764 ALMs
    Cyclone IV  -> 121 Mhz 3,608 LUT 2,082 FF 

От себя добавлю, что мне удавалось разгонять микропроцессорное ядро VexRiscv в варианте «small and productive with I$» на плате «Карно» до частоты 80 МГц. Более «жирные» ядра стабильно работают на частоте 50МГц на этой же плате.

12.2 Синтезируемые системы-на-кристалле на базе VexRiscv

Прежде чем мы углубимся во внутреннее устройство ядра VexRiscv, нужно сказать вот о чем. Наличие одного лишь синтезируемого микропроцессора мало чего дает пользователю, так как этот микропроцессор требуется обеспечить кодом исполняемой программы и каким-то количеством оперативной памяти для её работы, а чтобы эта программа могла выполнять какие-то полезные действия, требуется оснастить микропроцессор устройствами ввода/вывода. Собственно, любая ЭВМ, по классике — это процессор, соединенный с памятью и устройствами ввода/вывода посредством шины. Все это вместе помещенное на один кристалл принято называть «Системой-на-Кристалле» или СнК (от англ. «System-on-Chip» или SoC). Таковыми являются подавляющее большинство современных микропроцессоров и микроконтроллеров всех уровней и мастей. Микропроцессоров в чистом виде давно не осталось! Очевидно, что из «материи» ПЛИС можно собирать виртуальные системы-на-кристалле, которые принято называть «синтезируемой СнК» (synthesizable SoC). SpinalHDL вместе с вычислительным ядром VexRiscv предоставляют неплохой набор средств для построения синтезируемых СнК различной сложности, используя периферийные IP-блоки из библиотечной поставки SpinalHDL, и в этом смысле связка SpinalHDL+VexRiscv является самодостаточным решением, хотя и позволяет использовать любые сторонние IP-блоки, написанные на Verilog и VHDL. Вообще, будет правильнее сказать, что ядро VexRiscv является одним из библиотечных IP-блоков в составе фреймворка SpinalHDL.

Шарль Папон не поленился и снабдил пользователей ядра VexRiscv несколькими готовыми заготовками (или шаблонами) для написания своих синтезируемых СнК, это Murax SoC, Briey SoC и SaxonSoc. Первые две СнК входят в репозиторий VexRiscv и при сборке можно выбрать один из вариантов. SaxonSoc же более поздняя и более сложная реализация синтезируемой СнК, нацеленная, во-первых, на эксперименты с Banana Memory Bus (BMB), во-вторых, на «прощупывание» новых концепций построения СнК на основе «генераторов». Про BMB хочу сказать, что это тоже разработка Шарля Папона — весьма передовая шина, лишенная некоторых недостатков широко используемых шин AXI4 от ARM и Tilelink от SiFive, она проще, решает те же проблемы и является «FPGA friendly». Рассмотрим каждую из этих СнК поподробней.

Murex SoC

Murex SoC — самая манималистичная СнК по потребляемым ресурсам ПЛИС, может синтезироваться без каких-либо внешних компонентов и предоставляет следующие возможности:

  • Работает с ядрами VexRiscv конфигурации RV32I[M].

  • Имеет JTAG debugger (Eclipse/GDB/OpenOCD ready).

  • Поддерживает On-chip RAM (т. е. RAM, синтезированную внутри ПЛИС).

  • Один вход прерываний без контроллера.

  • Периферийная шина APB3.

  • 32 пиновый GPIO порт.

  • Один 16-ти битный prescaler, два 16-ти битных таймера.

  • UART с поддержкой FIFO для TX и RX.

СнК Murax SoC предназначена, по большей части, для тестирования самих ядер VexRiscv, но может быть использована для построение несложных микроконтроллерных систем для решения задач промышленной автоматизации. Далее я продемонстрирую один из вариантов такой системы, а также покажу, как можно расширить эту СнК дополнительными IP-блоками (контроллером прерываний, блоком статической памяти и FastEthernet).

Полная СнК Murax SoC с ядром VexRiscv с «bypass» стадиями при синтезировании для ПЛИС Lattice iCE40 умещается в 2800 логических ячеек и работает на частоте 66 МГц.

Briey SoC

Briey SoC — более навороченная СнК. Посмотрев на структурную схему Briey SoC, изображенную на рис. 20, можно легко заметить, что в основе этой СнК находится популярный кросс-коннект (шина) AXI4, с одной стороны к которому подключается ядро VexRiscv (двумя портами — для инструкций и данных раздельно), с другой — различная периферия и мосты на другие шины. Отличительная особенность данной СнК от Murax SoC состоит в том, что он позволяет подключать к вычислительному ядру различные расширения, а также кеши инструкций и данных. А еще Briey SoC имеет готовый интерфейс для подключения динамической памяти (SDRAM).

Но основное достоинство Briey SoC — это популярная шина AXI4, для которой не сложно найти практически любой периферийный IP-блок, как коммерческий, так и опенсорсный. В составе SpinalHDL, кстати, имеется достаточно богатая библиотека компонентов (IP-блоков), в том числе: FastEthernet (MII), I2C, JTAD, SIO, SPI, UART, USB, VGA, PLIC, мосты в другие шины (APB3, AMBA3, AMBA4, Wishbone) и много чего еще.

Более подробное описание Briey SoC можно найти в документации по ссылке:

https://spinalhdl.github.io/SpinalDoc-RTD/dev/SpinalHDL/miscelenea/lib/briey/hardware_toplevel.html

Рис. 20. Структура Briey SoC в составе VexRiscv.
Рис. 20. Структура Briey SoC в составе VexRiscv.

SaxonSoc

SaxonSoc — многопроцессорная СнК позволяющая запускать Linux и, наверное, этим все сказано. Замечу лишь, что в основе SaxonSoc находится разработанный автором свой кросс-коннект Banana Memory Bus, к которому подключаются периферийные устройства, а также мосты в другие шины. Один из вариантов конфигурации SaxonSoc представлен на рис. 21.

Рис. 21. Вариант конфигурации SaxonSoc.
Рис. 21. Вариант конфигурации SaxonSoc.

SaxonSoc находится в отдельном репозитории по адресу: https://github.com/SpinalHDL/SaxonSoc

Видео демонстрация работы SaxonSoc от автора:

Вообще, понятие СнК в рамках связки SpinalHDL/VexRiscv очень размытое. Можно взять любую из этих трех готовых СнК, убрать из неё всё ненужное, добавить нужное и получить структуру синтезируемой ЭВМ под свои задач. Далее мы этим и займемся, но сначала рассмотрим, что же такое SpinalHDL и чем он так хорош.

13. Язык описания аппаратуры SpinalHDL

Язык описания аппаратуры SpinalHDL был разработан в 2014 году. Это язык позволяющий описывать цифровые схемы на уровне регистровых передач (RTL). SpinalHDL представляет собой набор классов и библиотек для языка программирования Scala, который в свою очередь входит в экосистему ЯП Java, иными словами SpinalHDL — это фреймворк. Но не спешите воротить нос. При работе со SpinalHDL программировать на Scala вам скорее всего не придется, так как от Scala в SpinalHDL остались, пожалуй, только фигурные скобки. SpinalHDL перегружает и переопределяет действия многих операторов, делая синтаксис очень близким к VHDL, но при этом существенно менее многословным и лаконичным, т. е. текста писать придется существенно меньше. Вот что сам автор говорит про изобретенный им язык:

Традиционно языки HDL [имеется в виду Verilog и VHDL] предлагают небольшой выбор высокоуровневых абстракций, что приводит к длинным и повторяющимся описаниям сигнальных линий. Разработка сложной аппаратуры на таких языках требует много времени, а отладка и сопровождение очень затруднено из-за огромного количества запутанных сигналов. SpinalHDL позволяет разработчикам контролировать сложность посредством высокоуровневых абстракций, используя модели объектно-ориентированного и функционального программирования, создавая краткое самодокументируемое описание аппаратуры…

– Шарль Папон, автор и создатель SpinalHDL.

От себя добавлю, что в среде разработчиков на SystemVerilog и VHDL есть очень много ритуальных действий, например, описания цепей асинхронного сброса, создание отдельного процесса process/always для каждого регистра и т. д. Всего этого в SpinalHDL нет.

SpinalHDL был не первым HDL на основе Scala. Существует и успешно развивается такой HDL язык как Chisel брат близнец, изначально созданный в UC Berkeley и долгое время используемый в академической среде. Chisel в какой-то момент был подхвачен компанией SiFive, теми же людьми, что разрабатывали архитектуру RISC-V, и сейчас имеет определенное хождение в кругах ASIC разработчиков (вычислительные ядра от SiFive написаны на Chisel). В этой связи многие задают автору языка SpinalHDL вопрос «а нафига, если есть/был Chisel ?». И вот его ответ:

При создании SpinalHDL я, безусловно, был вдохновлен языком Chisel, но не подумайте, что SpinalHDL является прямым его ответвлением [fork-ом]. Chisel создавался и используется в основном для разработки микросхем (ASIC), он находится в состоянии активной разработки с постоянно изменяющимся API. SpinalHDL же нацелен больше на разработку для ПЛИС и, в общем-то, уже «заморожен» [не претерпевает изменений]. Язык активно сопровождается — устраняются ошибки, иногда добавляются новые фичи, но основное ядро языка останется неизменным на ближайшее будущее.

– Шарль Папон, автор и создатель SpinalHDL.

SpinalHDL — это метаязык. Это означает, что код, написанный на SpinalHDL, должен быть скомпилирован и исполнен, а в процессе исполнения он генерирует текст на другом традиционном HDL языке. То есть, после запуска программы на SpinalHDL, результатом её работы становится текстовый файл (или набор файлов) на языке Verilog или VHDL (поддерживаются оба), которые далее подаются на вход синтезатору, например, Yosys.

SpinalHDL создавался для работы с уже существующими инструментами и богатой кодовой базой. В языке имеется возможность интегрировать в дизайн любой модуль написанный на Verilog или VHDL, обернув его в так называемый IP «черный ящик» («blackbox»), что позволяет далее использовать его как компонент SpinalHDL.

SpinalHDL содержит все необходимые средства для моделирования, симуляции и верификации дизайна. Вспомним, что SpinalHDL это Scala — очень мощный ООП. Это позволяет вести разработку сложных дизайнов на одном единственном языке, не прибегая к помощи других инородных средств, как это часто бывает среди разработчиков, использующих традиционные HDL — моделирование на C++, масса скриптов на Perl, Shell и Tcl и уж потом код на Verilog или VHDL.

Подобным же свойством обладает другой широко известный HDL язык — SystemC. Этот HDL, аналогично SpinalHDL, является набором классов к языку C++ и также является метаязыком — очень мощный инструмент, используемый верификаторами. На мой взгляд, код на SystemC — это что-то ужасное: он не читаем, его очень много, синтаксис нелогичен и пестрит макросами. При решении этих же задач на SpinalHDL код получается существенно короче, проще для понимания и просто красивее. На Habr-е, кстати, есть отличная статья про SystemC от пользователя @Daffodil:

Разработка цифровой аппаратуры на C++/SystemC глазами SystemVerilog программиста

Всё это дает толчок к созданию языков мета-мета уровня. Например, известен язык (фреймворк) LiteX, который является библиотекой для Python, позволяющей описывать цифровые схемы используя готовые блоки, в том числе на SpinalHDL, и получать синтезируемый код.

Общими мазками процесс разработки на SpinalHDL выглядит следующим образом:

1. Разработчик описывает свой RTL дизайн на SpinalHDL и получает .scala файл (или файлы).

2. Компилирует .scala код с помощью Scala Build Tools (SBT) в исполняемый JVM.

3. Исполняет полученный байт-код с помощью Java машины и получает на выходе набор файлов на традиционном Verilog (или VHDL).

4. Подает сгенерированные Verilog/VHDL файлы на вход синтезирующему тулу (Yosys, Quartus, Vivado и т. д.), получая на выходе битстрим для ПЛИС.

Рис. 22. Процесс разработки на SpinalHDL в общих мазках.
Рис. 22. Процесс разработки на SpinalHDL в общих мазках.

Автор языка утверждает, что разработка на SpinalHDL не вносит в логику схемы никакой дополнительной избыточности (no logic overhead), а сгенерированный Verilog/VHDL код может быть успешно симулирован и верифицирован в том числе стандартными тулами. В сгенерированном коде сохраняются именования сигналов, регистров и модулей, описанные в коде на SpinalHDL.

Немного забегая вперед, для того чтобы продемонстрировать насколько SpinalHDL удобный и лаконичный язык, заточенный именно под RTL, я приведу пример схемы и кода, взятых прямо из документации. Предположим, что у нас имеется следующая цифровая схема (рис. 23), в которой присутствуют комбинационная логика и два регистра-счетчика: один с асинхронным сбросом, другой без такового.

Рис. 23. Пример цифровой схемы. Взято из документации на SpinalHDL.
Рис. 23. Пример цифровой схемы. Взято из документации на SpinalHDL.

При разработке на традиционных HDL языках разработчик обычно описывает несколько отдельных «процессов» с указанием списка «чувствительности» (перечень сигналов, которые, по его мнению, будут изменяться в процессе), при этом для комбинационной и последовательностной логики процессы как правило разделяют. Так же, на каждый из двух регистров описывается отдельный процесс, так как в данном случае регистры разных типов (со сбросом и без). Таким образом, у нас получается код на VHLD из трех процессов:

signal mySignal : std_logic;
signal myRegister : unsigned(3 downto 0);
signal myRegisterWithReset : unsigned(3 downto 0);

process(cond)
begin
    mySignal <= '0';
    if cond = '1' then
        mySignal <= '1';
    end if;
end process;

process(clk)
begin
    if rising_edge(clk) then
        if cond = '1' then
            myRegister <= myRegister + 1;
        end if;
    end if;
end process;

process(clk,reset)
begin
    if reset = '1' then
        myRegisterWithReset <= 0;
    elsif rising_edge(clk) then
        if cond = '1' then
            myRegisterWithReset <= myRegisterWithReset + 1;
        end if;
    end if;
end process;

На SpinalHDL эта же самая цифровая схема будет описана следующим образом:

val mySignal             = Bool()
val myRegister           = Reg(UInt(4 bits))
val myRegisterWithReset  = Reg(UInt(4 bits)) init(0)

mySignal := False
when(cond) {
    mySignal            := True
    myRegister          := myRegister + 1
    myRegisterWithReset := myRegisterWithReset + 1
}

Видно, что, во-первых, в коде на SpinalHDL нет процессов (они скрыты внутри типов конструкторов соответствующих переменных). Во-вторых, весь код комбинационной и последовательностной логики описывается в одном блоке под одним условным оператором when(). Я уверен, что для большинства разработчиков, давно пишущих код на Verilog и/или VHDL, такое решение будет просто шокирующим. На SpinalHDL такой стиль является нормой, и он позволяет существенно сократить число строк кода, а сам код сделать более понятным и читаемым.

13.1 Базовые конструкции языка SpinalHDL

Детально описывать все конструкции и возможности SpinalHDL я не буду, а пройдусь по основным моментам, чтобы у читателя составилось общее представление о языке. Заинтересовавшиеся читатели могут найти полный текст документации по ссылке:

https://spinalhdl.github.io/SpinalDoc-RTD/master/index.html

В SpinalHDL приняты аналогичные правила работы с сигналами, как в Verilog и VHDL, а именно:

  1. Все сигналы и регистры работают параллельно, порядок присваивания не имеет значения.

  2. Присваивание комбинационному сигналу выполняется (становится доступно) в этом же такте.

  3. Присваивание регистру выполняется в следующем такте того домена, к которому принадлежит регистр.

  4. При многократном присваивании в один и тот же сигнал или регистр, в расчет берется самое последнее присваивание (принцип «last assignment wins»).

  5. С любым регистром или сигналом можно работать как с объектом.

Типы данных

SpinalHDL является строго типизированным языком, в котором определены пять базовых типов и два составных типа. Базовые типы это: Bool, Bits, UInt для беззнаковых, SInt для знаковых и Enum. Два составных типа это Bundle — сложная структура сигналов, и Vec — массив однотипных сигналов. Для базовых типов можно указывать размерность в битах.

Для преобразования типов данных существуют методы вида .asBits(), .asUInt(), .asSInt() и .asBool().

Регистры

Для описания регистров в SpinalHDL используется специальные конструкции:

Reg(type) — описывает регистр указанного базового типа type.

RegInit(resetValue) — описывает регистр с начальным значением resetValue.

RegNext(nextValue) — описывает регистр, который каждый новый такт принимает значение, вычисляемое выражением nextValue.

RegNextWhen(nextValue, cond) — аналогично RegNext, но при условии, что выражение cond является истиным.

Присваивание :=

В SpinalHDL для синтезируемых переменных используется один тип оператора присваивания. Будет ли он блокирующим или неблокирующим определяется типом переменной. Существует модификация оператора присваивания: <> - позволяет присваивать сложные структуры поименованных сигналов (бандлы), учитывая направленность сигналов внутри бандла.

В вышеприведенном примере переменная mySignal описывает сигнал размерностью один бит, а переменные myRegister и myRegisterWithReset описывают регистры по 4 бита. Дополнительный модификатор init() указывает на то, что данный регистр имеет асинхронный сброс и устанавливает значение регистра по умолчанию. Установка дефолтных значений возможна для большинства современных ПЛИС и такой код успешно синтезируется.

Сравнение === и =/=

В SpinalHDL для сравнения двух сигналов используется оператор ===. Оператор с противоположной логикой имеет вид =/=. Пример:

io.flag  := (counter === 0) | io.cond1

Конкатенация ##

io.result := io.high ## io.low

Арифметические и логические операторы

Как и в других HDL языках, в SpinalHDL имеется множество операторов для описания арифметических и логических операций над сигналами. В таблицах ниже приведена часть из них:

Модификаторы in, out, master, slave

Модификаторы применяются к синтезируемым переменным (сигналам) и позволяют определить их логическое назначение, а также семантику для оператора <>.

Компоненты

В SpinalHDL компонент — это законченный логический блок цифровой схемы (аналогично module в Verilog). Любой компонент является классом-наследником от класса Component. Компонент может содержат член класса io типа Bundle — сложная структура, описывающая сигналы ввода/вывода данного компонента. Например, компонент (модуль) из схемы представленной на рис. 24 на SpinalHDL будет выглядеть следующим образом:

Рис. 24. Комбинационная схемы как компонент.
Рис. 24. Комбинационная схемы как компонент.
class MyComponent extends Component {
  val io = new Bundle {
    val a = in Bool
    val b = in Bool
    val c = in Bool
    val result = out Bool
  }
  val a_and_b = Bool
  a_and_b := io.a & io.b
  
  val not_c = ! io.c
  io.result := a_and_b | not_c
}

Этот код на SpinalHDL в результате генерации даст нам следующий код на VHDL:

entity MyComponent is
  port(
    io_a : in std_logic;
    io_b : in std_logic;
    io_c : in std_logic;
    io_result : out std_logic
  );
end MyComponent;
architecture arch of MyComponent is
  signal a_and_b : std_logic;
  signal not_c : std_logic;
begin
  io_result <= (a_and_b or not_c);
  a_and_b <= (io_a and io_b);
  not_c <= (not io_c);
end arch;

Области

Для удобства структурирования кода внутри компонента его части можно разбивать на области, используя конструкцию Area {...}, которая имеет следующий вид:

class TopLevel extends Component {
  //…
  val logicArea = new Area {
    val flag = Bool
  }
  val fsmArea = new Area {
    when(logicArea.flag) {
      //…
    }
  }
}

Инкапсулирование компонентов

Один компонент может быть инкапсулирован внутрь другого, при этом связь интерфейсных сигналов осуществляется оператором одиночного := или группового <> присваивания. Пример:

class SubComponent extends Component{
  val io = new Bundle {
    val input = in Bool
    val result = out Bool
  }
  ...
}
class TopLevel extends Component {
  val io = new Bundle {
    val a = in Bool
    val b = in Bool
    val result = out Bool
  }
val sub = new SubComponent
  sub.io.input := io.a
  io.result := sub.io.result | io.b
}

Оператор when(), elsewhen() и otherwise()

Данные операторы позволяют выполнять сравнение и условное назначение сигналов:

when(io.conds(0)){
  io.result := 2
  when(io.conds(1)){
    io.result := 1
  }
} elsewhen(1) {
  io.result := 3
} otherwise {
  io.result := 0
}

Оператор switch()

switch(state) {
  is(MyEnum.state0) {
  	...
  }
  is(MyEnum.state1) {
  	...
  }
  default{
  	...
  }
}

Оператор mux()

Упрощенная запись для описания мультиплексоров:

val bitwiseSelect = UInt(2 bits)
val bitwiseResult = bitwiseSelect.mux(
  0 -> (io.src0 & io.src1),
  1 -> (io.src0 | io.src1),
  2 -> (io.src0 ^ io.src1),
  default -> (io.src0)
)

Память Mem()

Массивы ячеек памяти могут быть определены специальным компонентом Mem(), который имеет встроенные методы для синхронного и асинхронного доступа:

//Memory of 1024 Bools
val syncRam = Mem(Bool, 1024)
val asyncRam = Mem(Bool, 1024)

//Write them
syncRam(5) := True
asyncRam(5) := True

//Read them
val syncRam = mem.readSync(6)
val asyncRam = mem.readAsync(4)

Функции и пользовательские операторы

В SpinalHDL можно определять и переопределять операции над объектами (сигналами, регистрами и сложными структурами данных). Предположим, что у нас имеется некий комплексный сигнал Color, состоящий из трех компонент: r, g и b. Тогда можно определить класс для работы с этим сигналом цветности, а внутри него определить оператор + для покомпонентного суммирования цветов. Такой класс приведен ниже.

case class Color(channelWidth: Int) extends Bundle {
  val r,g,b = UInt(channelWidth bits)
  def +(that: Color): Color = {
    val result = Color(channelWidth)
    result.r := this.r + that.r
    result.g := this.g + that.g
    result.b := this.b + that.b
    return result
  }
}

В приведенном выше примере используется параметризация размерности составляющих сигналов, которая определяется параметром channelWidth при инстанциировании сигнала класса Color.

13.2 FSM, потоки, конвейеры, тактовые домены

Машины состояний (FSM)

Машины состояний в SpinalHDL могут быть определены как традиционным методом с помощью Enum и switch, так и специализированными средствами (классы State и StateMachine), позволяющими легко создавать вложенные и множественно-вложенные FSM с минимальным количеством кода. SpinalHDL не предъявляет каких-либо требований к стилю изложения FSM, их можно и нужно описывать так, как это делается на языках программирования. Внутри обработки состояний можно смешивать как сигналы, так и регистры, что контрастирует с принятыми принципами описания FSM в мире Verilog/VHDL. Ниже приведены два примера с использованием класса StateMachine.

// FSM style A
val fsm = new StateMachine{
  io.result := False
  val counter = Reg(UInt(8 bits)) init (0)
  val stateA : State = new State with EntryPoint{
    whenIsActive (goto(stateB))
  }
  val stateB : State = new State{
    onEntry(counter := 0)
      whenIsActive {
      counter := counter + 1
      when(counter === 4){
      goto(stateC)
      }
    }
    onExit(io.result := True)
  }
  val stateC : State = new State{
    whenIsActive (goto(stateA))
  }
}
// FSM style B

val fsm = new StateMachine{
  val stateA = new State with EntryPoint
  val stateB = new State
  val stateC = new State
  val counter = Reg(UInt(8 bits)) init (0)
  io.result := False
  
  stateA
    .whenIsActive (goto(stateB))
  
  stateB
    .onEntry(counter := 0)
    .whenIsActive {
      counter := counter + 1
      when(counter === 4){
        goto(stateC)
      }
    }
    .onExit(io.result := True)
  stateC
    .whenIsActive (goto(stateA))
}

Мне лично больше по душе вариант А из-за наличия фигурных скобок — они четко дают понять, где заканчивается обработка одного состояния и начинается обработка следующего. Вариант B это чистый ООП подход из длинных конструкций соединенных оператором точка. :-)

Интерфейс потока Flow

Класс Flow позволяет описать простой valid/payload протокол взаимодействия по шине, в котором передающая сторона (master устройство) подает данные на комплексный сигнал payload, при этом устанавливая valid, а принимающая сторона (slave устройство) считывает данные с payload и не имеет возможности остановить работу шины. Внутри библиотек SpinalHDL описание класса Flow выглядит примерно следующим образом:

case class Flow[T <: Data](payloadType: T) extends Bundle {
	val valid = Bool
	val payload = cloneOf(payloadType)
}

Потоки можно соединять в цепочки операторами >> и << образуя сложные комбинационные схемы, либо операторами <-< или >-> образуя конвейеры (т. е. разделенные регистрами).

Пример использования потока Flow:

case class FlowExample() extends Component {
  val io = new Bundle {
    val request = slave(Flow(Bits(8 bit)))
    val answer = master(Flow(Bits(8 bit)))
  }
  val storage = Reg(Bits(8 bit))

  val fsm = new StateMachine {
    io.answer.setIdle()

    val idle: State = new State with EntryPoint {
      whenIsActive {
        when(io.request.valid) {
          storage := io.request.payload
          goto(sendEcho)
        }
      }
    }

    val sendEcho: State = new State {
      whenIsActive {
        io.answer.push(storage)
        goto(idle)
      }
    }
  }
}

В данном примере описана машина состояний, которая постоянно принимает данные по линии request и возвращает их же в ответе answer, сохраняя полученные данные во внутренний регистр. Так как сигналы request и answer объявлены как потоки, то весь код машины состояний может быть заменен одним простым выражением:

io.answer <-< io.request

Интерфейс потока Stream

Класс Stream позволяет определить протокол вида valid/ready/payload с «рукопожатием», в котором принимающая сторона (slave) начинает обработку данных payload по сигналу valid, при этом поток останавливается до подачи принимающей стороной сигнала готовности ready. Внутри библиотек SpinalHDL поток Stream определяется следующим образом:

case class Stream[T <: Data](payloadType: T) extends Bundle {
  val valid = Bool
  val ready = Bool
  val payload = cloneOf(payloadType)
}

В цифровой схемотехнике такой протокол очень часто используется, например, для работы с FIFO или с простой периферией которая требует ожидания выполнения запроса.

Пример использования потока Stream:

class StreamFifo[T <: Data](dataType: T, depth: Int) extends Component {
  val io = new Bundle {
    val push = slave Stream (dataType)
    val pop = master Stream (dataType)
  }
  ...
}

Как и в случае с потоком Flow, потоки Stream можно объединять операторами << и >> или <-< и >->, создавая таким образом сложные конвейеры обработки данных. По мимо этого у потока Stream имеется ряд управляющих методов, позволяющих останавливать и перезапускать обработку данных в конвейере (haltWhen, throwWhen и т.д).

В документации на SpinalHDL приводится такой пример кода:

case class RGB(channelWidth : Int) extends Bundle {
  val red   = UInt(channelWidth bits)
  val green = UInt(channelWidth bits)
  val blue  = UInt(channelWidth bits)
  def isBlack : Bool = red === 0 && green === 0 && blue === 0
}
val source = Stream(RGB(8))
val sink   = Stream(RGB(8))
sink <-< source.throwWhen(source.payload.isBlack)

Эквивалентная схема к этому коду приведена на рис. 25 ниже.

Рис. 25. Пример построения конвейера обработки данных с использованием потока Stream.
Рис. 25. Пример построения конвейера обработки данных с использованием потока Stream.

Построение конвейерного байпаса с применением LatencyAnalysis()

Часто в цифровых схемах при построении конвейерной обработки возникает необходимость запустить часть данных в обход нескольких ступеней конвейера, число которых неизвестно или может изменяться в процессе работы над схемой, например, как изображено на рис. 26.

Рис. 26. Конвейер неизвестной длины с байпассом на сдвиговом регистре.
Рис. 26. Конвейер неизвестной длины с байпассом на сдвиговом регистре.

Для того, чтобы обеспечить обход такого конвейера, в SpinalHDL имеется функция LatencyAnalysis(), вычисляющая кратчайший путь по описанной схеме, выраженный в количестве тактов. Тогда построение байпаса для такого конвейера будет выглядеть следующим образом:

val a = UInt(8 bits)
val b = UInt(8 bits)
val aCalcResult = complicatedLogic(a)
val aLatency = LatencyAnalysis(a,aCalcResult)
val bDelayed = Delay(b,cycleCount = aLatency)
val result = aCalcResult + bDelayed

Здесь complicatedLogic представляет собой объект, в котором скрывается схема конвейера: это может быть Component, Area или какая-то еще сущность. Функция Delay() добавляет последовательно указанное количество регистров, рассчитанное функцией LatencyAnalysis(), создавая таким образом сдвиговый регистр нужной глубины для обхода конвейера.

Тактовые домены и CDC

Любой цифровой дизайн является простым до тех пор, пока в нём не появляются участки схемы, тактируемые от разных источников тактового сигнала, причем это могут быть сигналы «как бы» одной и той же частоты, но если они поддерживаются не одним и тем же «фундаментом» (fundamental), то в тактовых сигналах двух разных источников очень быстро накапливается разность фаз, что ведет к появлению проблем метастабильности и потере данных. Сам процесс передачи данных из одного участка схемы в другой с отличным источником тактового сигнала принято называть «пересечением тактовых доменов» («Clock Domain Crossing» или CDC). В ряде случаев проблемы CDC можно решить достаточно просто, введя серию последовательных буферных регистров, но часто на стыке между двумя такими блоками требуется установка синхронизирующих устройств, например — FIFO со счетчиком на основе кода Грея. Не стану вдаваться в подробности решения этой непростой задачи, вместо этого приведу ссылку на известную статью Клиффорда Каммингса «Clock Domain Crossing (CDC) Design & VerificationTechniques Using SystemVerilog», в которой автор предлагает несколько различных решений, в том числе и с помощью FIFO.

Решение проблем CDC может выглядеть весьма громоздким, а разработчику приходится всё время держать в голове этот момент и писать код так, чтобы случайно не потерять момент из виду и не напортачить. Помимо этого, часто код приходится рубить на куски ifdef-ами — на синтезируемый и симулируемый. В итоге код превращается в отличную вермишель.

Чтобы справиться с вермишелью, SpinalHDL предлагает следующее решение — весь код нужно ассоциировать с каким-то соответствующим тактовым доменом, после чего, при генерации SpinalHDL будет автоматически подставлять требуемые тактовые сигналы и сигнал сброса в нужные участки кода на Verilog/VHDL, а также предпринимать действия по недопущению ошибки при CDC. Для этого имеется следующий инструментарий:

Класс ClockDomain — описывает параметры нового тактового домена, его сигналы тактирования и сброса. Надо заметить, что в SpinalHDL сигналы сброса и тактирования, как неразлучная пара, всегда ходят вместе.

Класс ClockingArea — в него включают весь код, относящийся к определенному тактовому домену.

Пример описания домена:

// Configure new clock domain
  val myClockDomain = ClockDomain(
    clock  = io.clk,
    reset  = io.resetn,
    config = ClockDomainConfig(
      clockEdge        = RISING,
      resetKind        = ASYNC,
      resetActiveLevel = LOW
    )
  )

  // Define an Area which uses myClockDomain
  val myArea = new ClockingArea(myClockDomain) {
    val myReg = Reg(UInt(4 bits)) init(7)

    myReg := myReg + 1

    io.result := myReg

    // ...
  }

Структура ClockDomainConfig задает ряд параметров, определяющих поведение SpinalHDL при генерации кода для указанного домена: тип фронта тактового сигнала и тип сигнала сброса.

По умолчанию, если привязка кода к тактовым доменам не выполнена, то весь код автоматически привязывается к дефолтному домену ClockDomain со следующими настройками:

  • Clock : rising edge

  • Reset : asynchronous, active high

  • No clock enable

CDC переход между двумя тактовыми доменами выполненный с помощью буферных регистров может выглядеть так:

class CrossingExample(clkA : ClockDomain,clkB : ClockDomain) extends Component {
  val io = new Bundle {
    val dataIn  = in Bool()
    val dataOut = out Bool()
  }

  // sample dataIn with clkA
  val area_clkA = new ClockingArea(clkA) {
    val reg = RegNext(io.dataIn) init(False)
  }

  // BufferCC to avoid metastability issues
  val area_clkB = new ClockingArea(clkB) {
    val buf1   = BufferCC(area_clkA.reg, False)
  }

  io.dataOut := area_clkB.buf1
}

Функция BufferCC() добавляет в генерируемый код два и более буферных регистра, тактируемых от тактового сигнала соответствующего домена, и работает только для однобитных сигналов. Для более сложных переходов имеется класс StreamFifoCC, с помощью которого можно легко описать CDC переход для сигналов любой сложности.

Пример:

val clockA = ClockDomain(???)
val clockB = ClockDomain(???)
val streamA,streamB = Stream(Bits(8 bits))
//...
val myFifo = StreamFifoCC(
  dataType  = Bits(8 bits),
  depth     = 128,
  pushClock = clockA,
  popClock  = clockB
)
myFifo.io.push << streamA
myFifo.io.pop  >> streamB

На этом, пожалуй, закончим наш краткий обзор языка SpinalHDL и перейдем к практическим упражнениям.

13.3 Установка SpinalHDL

Далее я буду рассматривать вариант подготовки среды для работы из командной строки в UNIX системах с использованием опенсорсного тулчейна Yosys и симулятора Verilator. Процесс установки и проверки работоспособности этих тулов описан в главе «6. Как установить утилиты тулчейна Yosys». SpinalHDL работает как на Linux, так и в BSD системах.

Как уже отмечалось выше, SpinalHDL это набор библиотек классов для языка программирования Scala, а это значит, что процесс сборки проекта целесообразно выполнять с помощью Scala Build Tools — sbt, все примеры и шаблоны автор SpinalHDL приводит для SBT. Для Scala, в свою очередь, требуется Java DK. Поэтому последовательно установим Java, Scala и SBT.

rz@devbox:~$ sudo apt-get update
rz@devbox:~$ sudo apt-get install openjdk-8-jdk
rz@devbox:~$ sudo apt-get install scala
rz@devbox:~$ sudo apt-get install sbt

Если в вашем дистрибутиве Linux по умолчанию нет SBT, то нужно разобраться, как его добавить. Для старых версий Ubuntu это делается так:

rz@devbox:~$ echo "deb https://dl.bintray.com/sbt/debian /" | sudo tee -a /etc/apt/sources.list.d/sbt.list
rz@devbox:~$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2EE0EA64E40A89B84B2DF73499E82A75642AC823
rz@devbox:~$ sudo apt-get update
rz@devbox:~$ sudo apt-get install sbt

На ОС FreeBSD 13.x установить всё это добро можно одной командой, так как Java, Scala и SBT уже присутствуют в репозитории с пакетами:

rz@butterfly:~ % sudo pkg install scala sbt

В случае возникновения затруднений с установкой SBT, рекомендую к прочтению статью на Хабре «Введение в sbt» от пользователя @antaresm.

Далее необходимо клонировать из репозитория шаблон SpinalHDL проекта, после чего зайти в каталог SpinalTemplateSbt и запустить генерацию Verilog кода для этого демонстрационного (шаблонного) проекта. SBT добудет все необходимые зависимости и установит их локально для пользователя, от которого выполняется команда, примерно вот так:

Клонирование и сборка репозитория SpinalTemplateSbt
rz@devbox:~ $ git clone https://github.com/SpinalHDL/SpinalTemplateSbt.git 
Cloning into 'SpinalTemplateSbt'... 
...
Resolving deltas: 100% (203/203), done. 

rz@devbox:~/SpinalTemplateSbt $ sbt "runMain projectname.MyTopLevelVerilog" 
[info] welcome to sbt 1.6.0 (OpenJDK BSD Porting Team Java 1.8.0_392) 
[info] loading settings for project spinaltemplatesbt-build from plugins.sbt .. 
[info] loading project definition from /usr/home/rz/SpinalTemplateSbt/project 
[info] loading settings for project projectname from build.sbt ... 
[info] set current project to projectname (in build file:/usr/home/rz/SpinalTemplateSbt/) 
[info] running (fork) projectname.MyTopLevelVerilog 
[info] [Runtime] SpinalHDL v1.10.0    git head : 270018552577f3bb8e5339ee2583c9c22d324215 
[info] [Runtime] JVM max memory : 4512.0MiB 
[info] [Runtime] Current date : 2024.01.24 03:53:05 
[info] [Progress] at 0.000 : Elaborate components 
[error] [Thread-2] INFO net.openhft.affinity.Affinity - Using dummy affinity control implementation 
[info] [Progress] at 0.190 : Checks and transforms 
[info] [Progress] at 0.271 : Generate Verilog 
[info] [Done] at 0.323 
[success] Total time: 2 s, completed Jan 24, 2024 3:53:06 AM 

В результате, в файле ./hw/gen/MyTopLevel.v мы получим код на языке Verilog:

./hw/gen/MyTopLevel.v
// Generator : SpinalHDL v1.10.0    git head : 270018552577f3bb8e5339ee2583c9c22d324215 
// Component : MyTopLevel 
// Git hash  : 51249d4c6e34bfcda55c0332356bd15cf5adbc1d 

`timescale 1ns/1ps 

module MyTopLevel ( 
  input  wire          io_cond0, 
  input  wire          io_cond1, 
  output wire          io_flag, 
  output wire [7:0]    io_state, 
  input  wire          clk, 
  input  wire          reset 
); 

  reg        [7:0]    counter; 

  assign io_state = counter; 
  assign io_flag = ((counter == 8'h00) || io_cond1); 
  always @(posedge clk or posedge reset) begin 
    if(reset) begin 
      counter <= 8'h00; 
    end else begin 
      if(io_cond0) begin 
        counter <= (counter + 8'h01); 
      end 
    end 
  end 


endmodule

Этот код соответствует следующему исходному коду на SpinalHDL находящемуся в файле ./hw/spinal/projectname/MyTopLevel.scala

./hw/spinal/projectname/MyTopLevel.scala
package projectname 

import spinal.core._ 

// Hardware definition 
case class MyTopLevel() extends Component { 
  val io = new Bundle { 
    val cond0 = in  Bool() 
    val cond1 = in  Bool() 
    val flag  = out Bool() 
    val state = out UInt(8 bits) 
  } 

  val counter = Reg(UInt(8 bits)) init 0 

  when(io.cond0) { 
    counter := counter + 1 
  } 

  io.state := counter 
  io.flag := (counter === 0) | io.cond1 
} 
object MyTopLevelVerilog extends App { 
  Config.spinal.generateVerilog(MyTopLevel()) 
} 

object MyTopLevelVhdl extends App { 
  Config.spinal.generateVhdl(MyTopLevel()) 
} 

Аналогично можно выполнить генерацию кода VHDL:

rz@devbox:~/SpinalTemplateSbt $ sbt "runMain projectname.MyTopLevelVhdl" 

rz@devbox:~/SpinalTemplateSbt $ ll ./hw/gen/MyTopLevel.v hdl
-rw-r--r--  1 rz  rz  14724 Jan 24 03:51 ./hw/gen/MyTopLevel.vhd

Код на VHDL получается очень огромный, так как SpinalHDL включит в него массу инструментальных (вспомогательных) функций. Я подозреваю, что автор языка SpinalHDL изначально ориентировался на генерацию Verilog и его языковые конструкции, поэтому SpinalHDL выдает коротенький Verilog и страшно длинный VHDL.

Далее мы разберем этот шаблонный пример несколько более детально. Я, в след за автором SpinalHDL, тоже буду ориентироваться на Verilog, уж извините.

13.4 Разбор шаблона SpinalTemplateSbt

Структура дерева каталогов этого шаблонного проекта, который по умолчанию называется projectname, достаточно сложная, но в нём присутствует всё, для того чтобы начать разрабатывать свою первую цифровую схему, верифицировать, симулировать и синтезировать её.

Начнем с описания некоторых настроечных файлов.

Файл ./build.sbt — конфигурационный и сборочный файл для SBT, содержит набор переменных, задающих версию языка Scala и версию библиотеки SpinalHDL, которая должна быть использована для компиляции данного проекта. Внутри файла build.sbt можно изменить имя проекта с используемого по умолчанию projectname на что-то своё (например, на myproject), но делать это совершенно не обязательно — это имя не влияет на конечный результат. Если Вы изменили имя проекта, то нужно создать новый рабочий каталог ./hw/spinal/myproject/ и все ваши исходные коды на SpinalHDL складывать в него. Во всех файлах исходных кодов после этого следует указывать package myproject в самой первой строке.

Файл ./build.sc — аналогичный для системы сборки Mill. Так как мы будем ориентироваться на SBT, то этот файл нам не потребуется и его можно смело удалить.

Каталог ./hw/spinal/projectname/ — рабочий каталог с исходными кодами проекта. По умолчанию в нём находятся следующие файлы:

rz@devbox:~/SpinalTemplateSbt $ ll ./hw/spinal/projectname/ 
total 16 
-rw-r--r--  1 rz  rz  338 Jan 24 03:48 Config.scala 
-rw-r--r--  1 rz  rz  580 Jan 24 03:48 MyTopLevel.scala 
-rw-r--r--  1 rz  rz  698 Jan 24 03:48 MyTopLevelFormal.scala 
-rw-r--r--  1 rz  rz  887 Jan 24 03:48 MyTopLevelSim.scala 

Файл ./hw/spinal/projectname/Config.scala — содержит настройки для Scala и должен всегда присутствовать в рабочем каталоге проекта. Если Вы переименовали свой проект в myproject, то необходимо скопировать этот файл в новый рабочий каталог и заменить в нем package projectname на package myproject.

Далее рассмотрим, какие файлы могут присутствовать в рабочем каталоге.

Файл MyTopLevel.scala — в нём должен присутствовать код компонента (модуля) самого верхнего уровня для вашего дизайна и ряд входных точек для синтеза: объекты MyTopLevelVerilog и MyTopLevelVhdl. Имена входных точек передаются в SBT в командной строке, например:

rz@devbox:~/SpinalTemplateSbt $ sbt "runMain projectname.MyTopLevelVerilog"
или
rz@devbox:~/SpinalTemplateSbt $ sbt "runMain projectname.MyTopLevelVhdl"

Файл MyTopLevelFormal.scala — может содержать код для выполнения формальной верификации дизайна и входную точку для неё. Для выполнения верификации необходимо, чтобы был установлен проект SymbiYosys и его основная утилита sby. Вообще, верификация — это тема отдельного длинного разговора, так что пока не будем её касаться.

Запуск верификации осуществляется командой:

rz@devbox:~/SpinalTemplateSbt $ sbt "runMain projectname.MyTopLevelFormal"

Файл MyTopLevelSim.scala — содержит код и точку входа для запуска и выполнения симуляции. Для выполнения симуляции должен быть установлен пакет Verilator. Эту интересную тему мы немного коснемся чуть ниже при работе с VexRiscv.

Запуск симуляции осуществляется командой:

rz@devbox:~/SpinalTemplateSbt $ sbt "runMain projectname.MyTopLevelSim"


Теперь посмотрим на файл с синтезируемым кодом. В заголовке этого файла мы видим стандартные для Scala элементы, описывающие имя пакета программ:

package projectname 

и перечень подключаемых библиотек:

import spinal.core._ 

В данном случае подключаются все библиотеки основного ядра SpinalHDL. Далее, при работе с VexRiscv, мы добавим сюда библиотеки софт-ядра, а также некоторые свои компоненты.

Далее идет описание компонента верхнего уровня, который должен быть унаследован от класса Component и его интерфейса ввода/вывода:

// Hardware definition 
case class MyTopLevel() extends Component { 
  val io = new Bundle { 
    val cond0 = in  Bool() 
    val cond1 = in  Bool() 
    val flag  = out Bool() 
    val state = out UInt(8 bits) 
  } 

В данном случае, для примера, предлагается цифровая схема с двумя входящими однобитными сигналами cond0 и cond1, и двумя выходными сигналами flag и state. Сигнал flag однобитный, а сигнал state имеет ширину 8 бит. Этому описанию соответствует следующий код на языке Verilog, из которого мы видим, что SpinalHDL неявно включает в компонент сигнал тактирования clk и асинхронного сброса reset:

module MyTopLevel ( 
  input  wire          io_cond0, 
  input  wire          io_cond1, 
  output wire          io_flag, 
  output wire [7:0]    io_state, 
  input  wire          clk, 
  input  wire          reset 
); 

Далее следует описание последовательностной и комбинационной логики:

val counter = Reg(UInt(8 bits)) init 0 

  when(io.cond0) { 
    counter := counter + 1 
  } 

  io.state := counter 
  io.flag := (counter === 0) | io.cond1

В данном случае создается 8-битный регистр-счетчик со сбросом counter, значение которого увеличивается на 1 каждый такт при наличии на входе cond0 лог «1». Выходной сигнал flag устанавливается в лог «1», если значение счетчика равно нулю и входной сигнал cond1 установлен в лог «1».

Этот код транслируется в следующий код на языке Verilog:

  reg        [7:0]    counter;

  assign io_state = counter; 
  assign io_flag = ((counter == 8'h00) || io_cond1); 
  always @(posedge clk or posedge reset) begin 
    if(reset) begin 
      counter <= 8'h00; 
    end else begin 
      if(io_cond0) begin 
        counter <= (counter + 8'h01); 
      end 
    end 
  end 

Видно, что SpinalHDL, следуя принятым в Verilog традициям, сначала размещает код для комбинационной логики, а затем добавляет код для последовательностной: запускается процесс обработки по передним фронтам тактового сигнала clk и сигнала сброса reset, внутри процесса добавляется код для асинхронного сброса. Генерируемый код на языке Verilog получается вполне читаемый и может быть далее использован как модуль в составе другого проекта, либо подан на вход синтезатору Yosys для получения битстрима для ПЛИС.

13.5 Подготовка Makefile-а для синтеза из SpinalHDL в битстрим

Договоримся называть «генерацией» процесс преобразования кода из языка SpinalHDL в код на языке Verilog. Чтобы из кода на языке SpinalHDL получить битстрим для ПЛИС на плате «Карно», необходимо последовательно выполнить следующие действия:

  1. Выполнить генерацию текстов Verilog из SpinalHDL с помощью утилиты sbt. На выходе получим один или несколько файлов Verilog (файлы с расширением .v).

  2. Выполнить синтез нетлиста из текстов Verilog с помощью утилиты yosys. На выходе получим нетлист в формате JSON (файл с расширением .json).

  3. Выполнить оптимизацию, размещение и трассировку нетлиста утилитой nextpnr-ecp5. На выходе получим текстовый файл размещения в формате Trellis (файл с расширением .config).

  4. Выполнить генерацию битстрима из файла размещения. На выходе получим двоичный файл (расширение .bit), готовый к загрузке в ПЛИС.

Воспользуемся наработками из главы «8. Типовой Makefile для синтеза с помощью Yosys», слегка упростим его для понимания, и получим следующий Makefile:

Makefile для SpinalTemplateSbt
NAME = projectname 
SPINALHDL = ./hw/spinal/projectname/MyTopLevel.scala 
VERILOG = ./hw/gen/MyTopLevel.v 
LPF = karnix_cabga256.lpf 
DEVICE = 25k 
PACKAGE = CABGA256 
FTDI_CHANNEL = 0 ### FT2232 has two channels, select 0 for channel A or 1 for channel B 
# 
FLASH_METHOD := $(shell cat flash_method 2> /dev/null) 
UPLOAD_METHOD := $(shell cat upload_method 2> /dev/null) 

all: $(NAME).bit 

$(VERILOG): $(SPINALHDL) 
        sbt "runMain projectname.MyTopLevelVerilog" 

$(NAME).bin: $(LPF) $(VERILOG) 
        yosys -v2 -p "synth_ecp5 -abc2 -top MyTopLevel -json $(NAME).json" $(VERILOG) 
        nextpnr-ecp5 --package $(PACKAGE) --$(DEVICE) --json $(NAME).json --textcfg $(NAME)_out.config --lpf $(LPF) --lpf-allow-unconstrained 
        ecppack --compress --freq 38.8 --input $(NAME)_out.config --bit $(NAME).bit 


upload_openloader: 
ifeq ("$(FLASH_METHOD)", "flash") 
        openFPGALoader -v --ftdi-channel $(FTDI_CHANNEL) -f --reset $(NAME).bit 
else 
        openFPGALoader -v --ftdi-channel $(FTDI_CHANNEL) $(NAME).bit 
endif 

clean: 
        @rm $(NAME).json $(NAME)_out.config $(NAME).bit $(VERILOG)

По мимо этого, для успешной генерации битстрима, нам необходимо создать .LPF файл с описанием внешних выводов микросхемы ПЛИС, распаянной на плате «Карно» и передать его имя в утилиту nextpnr-ecp5. Создадим файл karnix_cabga256.lpf исходя из следующих данных:

  1. На плате «Карно» тактовый генератор частотой 25.0 МГц подключен к выводу B9 микросхемы ПЛИС, обозначим этот вывод для входного тактового сигнала clk.

  2. Задействуем вывод E13, который на плате «Карно» подведен к кнопке KEY3 и подтянут к лог «0», как входной сигнал асинхронного сброса reset.

  3. Задействуем выводы B13 и C13, к которым подключены кнопки KEY0 и KEY1, как входные сигналы io_cond0 и io_cond1 соответственно.

  4. Задействуем вывод A13, к нему подключен светодиод LED0, как выходной сигнал io_flag.

  5. Задействуем выводы L1, L2, M1, M2, K4, K5, L4 и L4 как биты выходного сигнала io_state[7:0]. Эти выводы распаяны на плате «Карно» на линии GPIO_00GPIO_07.

  6. Все выводы имеют уровни напряжений +3,3В, т. е. соответствуют типу уровней LVCMOS33.

Тогда наш .LPF файл примет следующий вид:

karnix_cabga256.lpf
FREQUENCY PORT "clk" 25.0 MHz; 
LOCATE COMP "clk" SITE "B9"; 
IOBUF PORT "clk" IO_TYPE=LVCMOS33; 

LOCATE COMP "reset" SITE "E13"; 
IOBUF PORT "reset" IO_TYPE=LVCMOS33; 

LOCATE COMP "io_cond0" SITE "B13";             # KEY0 
IOBUF PORT "io_cond0" IO_TYPE=LVCMOS33; 

LOCATE COMP "io_cond1" SITE "C13";             # KEY1 
IOBUF PORT "io_cond1" IO_TYPE=LVCMOS33; 

LOCATE COMP "io_flag" SITE "A13";             # LED0 
IOBUF PORT "io_flag" IO_TYPE=LVCMOS33; 

LOCATE COMP "io_state[0]" SITE "L1";             # GPIO_00 
IOBUF PORT "io_state[0]" IO_TYPE=LVCMOS33; 
IOBUF PORT "io_state[0]" PULLMODE=NONE DRIVE=16; 
LOCATE COMP "io_state[1]" SITE "L2";             # GPIO_01 
IOBUF PORT "io_state[1]" IO_TYPE=LVCMOS33; 
IOBUF PORT "io_state[1]" PULLMODE=NONE DRIVE=16; 
LOCATE COMP "io_state[2]" SITE "M1";             # GPIO_02 
IOBUF PORT "io_state[2]" IO_TYPE=LVCMOS33; 
IOBUF PORT "io_state[2]" PULLMODE=NONE DRIVE=16; 
LOCATE COMP "io_state[3]" SITE "M2";             # GPIO_03 
IOBUF PORT "io_state[3]" IO_TYPE=LVCMOS33; 
IOBUF PORT "io_state[3]" PULLMODE=NONE DRIVE=16; 
LOCATE COMP "io_state[4]" SITE "K4";             # GPIO_04 
IOBUF PORT "io_state[4]" IO_TYPE=LVCMOS33; 
IOBUF PORT "io_state[4]" PULLMODE=NONE DRIVE=16; 
LOCATE COMP "io_state[5]" SITE "K5";             # GPIO_05 
IOBUF PORT "io_state[5]" IO_TYPE=LVCMOS33; 
IOBUF PORT "io_state[5]" PULLMODE=NONE DRIVE=16; 
LOCATE COMP "io_state[6]" SITE "L4";             # GPIO_06 
IOBUF PORT "io_state[6]" IO_TYPE=LVCMOS33; 
IOBUF PORT "io_state[6]" PULLMODE=NONE DRIVE=16; 
LOCATE COMP "io_state[7]" SITE "L5";             # GPIO_07 
IOBUF PORT "io_state[7]" IO_TYPE=LVCMOS33; 
IOBUF PORT "io_state[7]" PULLMODE=NONE DRIVE=16; 

Теперь можно собирать проект командой make и ожидать окончания сборки, которое сигнализируется следующим сообщением:

Info: Program finished normally. 
ecppack --compress --freq 38.8 --input projectname_out.config --bit projectname.bit

Если все прошло удачно, то можно подключить плату «Карно», загрузить битстрим в микросхему ПЛИС командой make upload и протестировать нашу цифровую схему нажимая кнопки KEY0 и KEY1 — мы должны наблюдать за реакцией на светодиоде LED0, а на выходах GPIO получать число срабатываний в двоичной форме. Результирующий битстрим будет находиться в файле projectname.bit .

Небольшое замечание по поводу сигналов сброса. На плате «Карно» уже имеется кнопка «RESET». Её нажатие приводит к системном сбросу микросхемы ПЛИС и инициации процесса считывания конфигурации из NOR flash памяти, что равносильно сбросу питания. Сигнал «reset», который мы определили выше и подвели на копку KEY3 — это сброс схемы пользователя внутри ПЛИС, фактически её нажатие будет приводить к обнулению только счетчика синтезированной цифровой схемы.

В следующей главе мы немного поэкспериментируем с кодом на SpinalHDL и проанализируем информацию об ошибках, выдаваемых компилятором Scala и генератором кода SpinalHDL. А пока что приведу ссылку на репозиторий модифицированного шаблонного проекта с Makefile-ом и .LPF для платы «Карно»:

https://github.com/Fabmicro-LLC/SpinalTemplateSbtForKarnix.git

13.6 Анализируем вывод сообщений SpinalHDL

Запустив процесс сборки, первым делом мы увидим вывод от компилятора Scala, который будет собирать наш проект вместе с библиотеками классов в составе SpinalHDL:

rz@devbox:~/SpinalTemplateSbt$ make 

sbt "runMain projectname.MyTopLevelVerilog" 
[info] welcome to sbt 1.6.0 (Ubuntu Java 11.0.9.1) 
[info] loading settings for project spinaltemplatesbt-build from plugins.sbt ... 
[info] loading project definition from /home/rz/SpinalTemplateSbt/project 
[info] loading settings for project projectname from build.sbt ... 
[info] set current project to projectname (in build file:/home/rz/SpinalTemplateSbt/) 
[info] running (fork) projectname.MyTopLevelVerilog 

Если компиляция прошла успешно, т. е. синтаксических ошибок с точки зрения Scala не выявлено, то SBT запустит на исполнение собранный байткод с помощью JVM и мы начнем получать сообщения от SpinalHDL о ходе процесса генерации:

[info] [Runtime] SpinalHDL v1.10.0    git head : 270018552577f3bb8e5339ee2583c9c22d324215 
[info] [Runtime] JVM max memory : 8294.0MiB 
[info] [Runtime] Current date : 2024.01.26 21:22:22 
[info] [Progress] at 0.000 : Elaborate components 
[info] [Progress] at 0.725 : Checks and transforms 
[info] [Progress] at 0.958 : Generate Verilog 
[info] [Done] at 1.140 
[success] Total time: 5 s, completed Jan 26, 2024, 9:22:23 PM 

Давайте попробуем внести в код приведенного выше примера какую-нибудь ошибку, скажем, попробуем присвоить переменной (сигналу) типа UInt значение типа Bool. Для этого исправим код следующим образом:

  val counter = Reg(UInt(8 bits)) init 0 

  io.state := True // <-- error here

  when(io.cond0) { 
    counter := counter + 1 
  } 

После запуска процесса сборки мы сразу получим сообщение от компилятора Scala следующего вида:

Compilation failed
rz@devbox:~/SpinalTemplateSbt$ make 

sbt "runMain projectname.MyTopLevelVerilog" 
[info] welcome to sbt 1.6.0 (Ubuntu Java 11.0.9.1) 
[info] loading settings for project spinaltemplatesbt-build from plugins.sbt ... 
[info] loading project definition from /home/rz/SpinalTemplateSbt/project 
[info] loading settings for project projectname from build.sbt ... 
[info] set current project to projectname (in build file:/home/rz/SpinalTemplateSbtForKarnix/) 
[info] compiling 1 Scala source to /home/rz/SpinalTemplateSbtForKarnix/target/scala-2.12/classes ... 
[error] /home/rz/SpinalTemplateSbtForKarnix/hw/spinal/projectname/MyTopLevel.scala:16:12: overloaded method value := with alternatives: 
[error]   (value: String)Unit <and> 
[error]   (rangesValue: (Any, Any),_rangesValues: (Any, Any)*)Unit <and> 
[error]   (that: spinal.core.UInt)(implicit loc: spinal.idslplugin.Location)Unit 
[error]  cannot be applied to (spinal.core.Bool) 
[error]   io.state := True 
[error]            ^ 
[error] one error found 
[error] (Compile / compileIncremental) Compilation failed 
[error] Total time: 7 s, completed Jan 26, 2024, 10:50:44 PM 
Makefile:15: recipe for target 'hw/gen/MyTopLevel.v' failed 
make: *** [hw/gen/MyTopLevel.v] Error 1 

Здесь Scala выдает четкое сообщение о том, где и когда произошла ошибка. Не забудем вернуть код в исходное состояние и продолжим эксперименты.

В процессе генерации SpinalHDL осуществляет ряд проверок нашего дизайна на логические проблемы. В частности, он может выявлять «защелки» и «петли» — это когда выход логической цепи завернут на её вход. Такие конструкции не должны присутствовать в нормальных цифровых схемах и создают ряд проблем (за исключением очень редких случаев, когда разработчик делает это целенаправленно). Сложность дизайна делает такие ошибки неочевидными и малозаметными, и они могут легко просочиться в готовое изделие, здесь SpinalHDL стоит на страже наших интересов.

Попробуем умышленно добавить «петлю» в код и посмотреть, как выглядит реакция SpinalHDL. Для этого исправим файл hw/spinal/projectname/MyTopLevel.scala так, чтобы тело компонента содержало следующий код:

val counter = Reg(UInt(8 bits)) init 0 
  val latch = Bool() 

  latch := ~io.flag 

  when(io.cond0) { 
    counter := counter + 1 
  } 

  io.state := counter 
  //io.flag := (counter === 0) | io.cond1 
  io.flag := (counter === 0) | io.cond1 | latch 

После запуска команды make мы увидим длинную портянку с различными сообщениями, среди которых будут сообщения об ошибке следующего вида:

COMBINATORIAL LOOP
[info] ********************************************************************************************** 
[info] [Warning] Elaboration failed (2 errors). 
[info]           Spinal will restart with scala trace to help you to find the problem. 
[info] ********************************************************************************************** 
[info] [Progress] at 0.902 : Elaborate components 
[info] [Progress] at 0.918 : Checks and transforms 
[error] Exception in thread "main" spinal.core.SpinalExit: 
[error]  Error detected in phase PhaseCheckCombinationalLoops 
[error] ******************************************************************************** 
[error] ******************************************************************************** 
[error] COMBINATORIAL LOOP : 
[error]   Partial chain : 
[error]     >>> (toplevel/io_flag : out Bool) at projectname.MyTopLevel$$anon$1.<init>(MyTopLevel.scala:10) >>> 
[error]     >>> (toplevel/latch :  Bool) at projectname.MyTopLevel.<init>(MyTopLevel.scala:15) >>> 
[error]     >>> (toplevel/io_flag : out Bool) at projectname.MyTopLevel$$anon$1.<init>(MyTopLevel.scala:10) >>> 
[error]   Full chain : 
[error]     (toplevel/io_flag : out Bool) 
[error]     ! Bool 
[error]     (toplevel/latch :  Bool) 
[error]     Bool || Bool 
[error]     (toplevel/io_flag : out Bool) 
[error] ********************************************************************************

Здесь SpinalHDL подсказывает нам номера строк кода (MyTopLevel.scala:10 и MyTopLevel.scala:15) в которых определены переменные, участвующие в создании «петли».

К слову сказать, утилита синтеза yosys тоже способна распознавать такие «петли», о чем выдает предупреждающее сообщения. Поэтому, давайте попробуем умышлено допустить ошибку, которую yosys не заметит, но SpinalHDL точно не пропустит. Для этого нам достаточно неполно определить состояние выходного сигнала io_flag, например вот так:

val counter = Reg(UInt(8 bits)) init 0 

  when(io.cond0) { 
    counter := counter + 1 
  } 

  io.state := counter 
  //io.flag := (counter === 0) | io.cond1 
  when(counter === 0 | io.cond1) { 
    io.flag := True 
  } 

Жирным шрифтом выделен участок проблемного кода. После запуска сборки мы получим от SpinalHDL следующее сообщение об ошибке:

LATCH DETECTED from the combinatorial signal
[info] ********************************************************************************************** 
[info] [Warning] Elaboration failed (2 errors). 
[info]           Spinal will restart with scala trace to help you to find the problem. 
[info] ********************************************************************************************** 
[info] [Progress] at 0.948 : Elaborate components 
[info] [Progress] at 0.968 : Checks and transforms 
[error] Exception in thread "main" spinal.core.SpinalExit: 
[error]  Error detected in phase PhaseCheck_noLatchNoOverride 
[error] ******************************************************************************** 
[error] ******************************************************************************** 
[error] LATCH DETECTED from the combinatorial signal (toplevel/io_flag : out Bool), defined at 
[error]     projectname.MyTopLevel$$anon$1.<init>(MyTopLevel.scala:10) 
[error]     projectname.MyTopLevel.<init>(MyTopLevel.scala:7) 
[error]     projectname.MyTopLevelVerilog$.$anonfun$new$3(MyTopLevel.scala:28) 
[error]     spinal.sim.JvmThread.run(SimManager.scala:51) 
[error] ******************************************************************************** 

Как и в предыдущий раз, SpinalHDL сообщает нам сначала номер строки, в которой определена переменная, создающая проблему, а далее номера строк блоков кода, где эта переменная используется. К сожалению, в данном случае SpinalHDL не даст точного указания на номер ошибочной строки кода, так как такой строки не существует — весь код корректный, некорректным является логика дизайнера.

Давайте исправим ошибку добавив обработку недостающего состояния следующим глупым, но наглядным способом:

  io.state := counter 
  //io.flag := (counter === 0) | io.cond1 
  when(counter === 0 | io.cond1) { 
    io.flag := True 
  } otherwise { 
    io.flag := False 
  } 

Здесь жирным текстом выделен добавленный код, исправляющий логическую ошибку. Запустим сборку и убедимся, что компиляция и генерация проходит успешно:

SUCCESS
rz@devbox:~/SpinalTemplateSbt$ make 
sbt "runMain projectname.MyTopLevelVerilog" 
[info] welcome to sbt 1.6.0 (Ubuntu Java 11.0.9.1)
[info] compiling 1 Scala source to /home/rz/SpinalTemplateSbt/target/scala-2.12/classes ... 
[info] running (fork) projectname.MyTopLevelVerilog 
[info] [Runtime] SpinalHDL v1.10.0    git head : 270018552577f3bb8e5339ee2583c9c22d324215 
[info] [Runtime] JVM max memory : 8294.0MiB 
[info] [Runtime] Current date : 2024.01.26 22:40:43 
[info] [Progress] at 0.000 : Elaborate components 
[info] [Progress] at 0.692 : Checks and transforms 
[info] [Progress] at 0.900 : Generate Verilog 
[info] [Done] at 1.036 
[success] Total time: 13 s, completed Jan 26, 2024, 10:40:44 PM

SpinalHDL может выдавать еще ряд ошибок, но приведенные выше две являются самыми распространенными, и с ними сталкивается любой новичок буквально при первой же попытке собрать свой проект. Надеюсь, мой беглый разбор поможет читателю избежать таких ситуаций, и он не остановится в своём желании пробовать SpinalHDL.

13.7 Симуляция и верификация в SpinalHDL

Несколько слов следует сказать о том, как реализована симуляция и верификация в SpinalHDL. Так как SpinalHDL на выходе генерирует Verilog код, то к его симуляции и верификации могут быть применены все имеющиеся традиционные средства. Однако, с некоторых пор в SpinalHDL был добавлен API, позволяющий создавать испытательные стенды (testbenches) прямо внутри дизайна, используя всю мощь языка Scala. Данный API позволяет читать/изменять сигналы внутри «подопытного» компонента, распараллеливать и, наоборот, объединять процессы симуляции, ожидать наступление определенного состояния, распечатывать информацию о процессе симуляции, состоянии регистров и сигналов испытуемого компонента и т. д.

Для выполнения симуляции SpinalHDL использует внешние средства, такие как Verilator и Icarus Verilog, установку этих тулов мы обсуждали ранее. Ниже я приведу пример тривиального тестбенча из документации на SpinalHDL.

В данном примере в качестве «подопытного кролика» предлагается простой компонент Indentity с параметром определяющим размерность своих сигналов в битах. Этот компонент имеет один входной сигнал a и один выходной сигнал z, который всегда повторяет входной сигнал. Код такого компонента выглядит следующим образом:

import spinal.core._

// Identity takes n bits in a and gives them back in z
class Identity(n: Int) extends Component {
  val io = new Bundle {
    val a = in Bits(n bits)
    val z = out Bits(n bits)
  }

  io.z := io.a
}

Предположим, что нам требуется испытать (верифицировать) работу компонента Identity для случая, когда размерность сигналов составляет три бита. Тогда испытательный стенд для него может выглядеть так:

import spinal.core.sim._

object TestIdentity extends App {
  // Use the component with n = 3 bits as "dut" (device under test)
  SimConfig.withWave.compile(new Identity(3)).doSim{ dut =>
    // For each number from 3'b000 to 3'b111 included
    for (a <- 0 to 7) {
      // Apply input
      dut.io.a #= a
      // Wait for a simulation time unit
      sleep(1)
      // Read output
      val z = dut.io.z.toInt
      // Check result
      assert(z == a, s"FAIL: Got $z, expected $a")
    }
    println(s"SUCCESS!")
  }
}

Данный тестбенч определяет испытуемый компонент dut типа Identity(3) и верифицирует его путем последовательного перебора всех допустимых входных значений dut.a и проверкой выходных значений dut.z, которые обязаны совпадать. Если будет выявлено несовпадение, то в лог будет выдано сообщение об ошибке (assert) и верификация будет остановлена. Если же перебор всех вариантов завершится успешно, то мы увидим в логе сообщение «SUCCESS!».

Добавим этот код в отдельный файл hw/spinal/projectname/TestIdentity.scala и запустим верификацию следующей командой:

sbt "runMain projectname.TestIdentity"
rz@devbox:~/SpinalTemplateSbt $ sbt "runMain projectname.TestIdentity" 

[info] welcome to sbt 1.6.0 (OpenJDK BSD Porting Team Java 1.8.0_392) 
[info] loading settings for project spinaltemplatesbt-build from plugins.sbt ... 
[info] loading project definition from /usr/home/rz/SpinalTemplateSbt/project 
[info] loading settings for project projectname from build.sbt ... 
[info] set current project to projectname (in build file:/usr/home/rz/SpinalTemplateSbt/) 
[info] running (fork) projectname.TestIdentity 
[info] [Runtime] SpinalHDL v1.10.1    git head : 2527c7c6b0fb0f95e5e1a5722a0be732b364ce43 
[info] [Runtime] JVM max memory : 4512.0MiB 
[info] [Runtime] Current date : 2024.03.15 03:33:52 
[info] [Progress] at 0.000 : Elaborate components 
[info] [Progress] at 0.176 : Checks and transforms 
[info] [Progress] at 0.259 : Generate Verilog 
[info] [Done] at 0.304 
[info] [Progress] Simulation workspace in /usr/home/rz/SpinalTemplateSbt/./simWorkspace/Identity 
[info] [Progress] Verilator compilation started 
[info] [info] Found cached verilator binaries 
[info] [Progress] Verilator compilation done in 311.979 ms 
[info] [Progress] Start Identity test simulation with seed 1948229477 
[info] SUCCESS! 
[info] [Done] Simulation done in 7.414 ms 
[success] Total time: 3 s, completed Mar 15, 2024 3:33:53 AM 

Здесь мы наблюдаем как SBT сгенерировал нам код для Verilator-а (обычно это код на языке Си, он располагается в подкаталоге ./simWorkspace/), запустил компиляцию этого кода и поставил его на исполнение. Успешный результат симуляции мы видим в виде текстового сообщения:

[info] SUCCESS! 

Но это еще не всё. В подкаталоге ./simWorkspace/Identity/test/ мы получим файл с временной диаграммой всех перебираемых в процессе верификации вариантов сигналов или состояний:

rz@devbox:~/SpinalTemplateSbtForKarnix $ ll simWorkspace/Identity/test/ 
total 4 
-rw-r--r--  1 rz  rz  548 Mar 15 03:33 wave.vcd 

Визуализировать (просмотреть в графическом виде) этот файл можно утилитой GTKWave:

rz@devbox:~/SpinalTemplateSbt $ gtkwave simWorkspace/Identity/test/wave.vcd

Так как испытуемый компонент Identity содержит исключительно комбинационную схему, то результат визуализации не очень интересен — не имеет разворота во времени. Далее, при работе с вычислительным ядром VexRiscv, я продемонстрирую как провести потактовую симуляцию ядра при исполнении программы, и мы посмотрим на некоторые временные диаграммы.

14. Синтез вычислительного ядра VexRiscv

14.1 Знакомство с репозиторием VexRiscv

Теперь, когда мы немного освоились с синтаксисом языка SpinalHDL, подготовили и проверили среду, научились находить и исправлять типовые ошибки, можно приступить к изучению синтезируемой ЭВМ на базе ядра VexRiscv.

Репозиторий с кодом VexRiscv находится по адресу: https://github.com/SpinalHDL/VexRiscv

Клонируем его и посмотрим, что там внутри:

Cloning into 'VexRiscv'...
rz@devbox:~$ git clone https://github.com/SpinalHDL/VexRiscv 
Cloning into 'VexRiscv'... 
...
Resolving deltas: 100% (9730/9730), done. 
rz@devbox:~$ cd  VexRiscv
rz@devbox:~/VexRiscv$ tree -d -L 5 
. 
├── assets 
├── doc 
│   ├── gcdPeripheral 
│   │   ├── img 
│   │   └── src 
│   │       └── main 
│   │           ├── c 
│   │           └── scala 
│   ├── nativeJtag 
│   ├── smp 
│   └── vjtag 
├── project 
├── scripts 
│   ├── Murax 
│   │   ├── arty_a7 
│   │   ├── iCE40-hx8k_breakout_board 
│   │   │   └── img 
│   │   ├── iCE40-hx8k_breakout_board_xip 
│   │   │   └── img 
│   │   └── iCE40HX8K-EVB 
│   └── regression 
└── src 
    ├── main 
    │   ├── c 
    │   │   ├── common 
    │   │   ├── emulator 
    │   │   │   ├── build 
    │   │   │   └── src 
    │   │   └── murax 
    │   │       ├── hello_world 
    │   │       └── xipBootloader 
    │   ├── ressource 
    │   │   └── hex 
    │   └── scala 
    │       ├── spinal 
    │       │   └── lib 
    │       └── vexriscv 
    │           ├── demo 
    │           ├── ip 
    │           ├── plugin 
    │           └── test 
    └── test 

Из этого разветвленного дерева подкаталогов нас на первых порах будут интересовать только подкаталоги ./scripts/Murax, ./src/main/scala и ./src/main/c.

Подкаталог ./scripts/Murax содержит ряд скриптов для сборки нескольких примеров с использованием СнК Murax для нескольких плат с микросхемами ПЛИС.

Подкаталог ./src/main/scala содержит код на языке SpinalHDL, в том числе реализацию ядра VexRiscv, нескольких вариантов СнК на его базе и код периферийных устройств задействованных в этих СнК.

Подкаталог ./src/main/c содержит исходные коды на языке Си нескольких примеров программ, которые можно исполнить на данном синтезируемом микропроцессоре.

Перед тем, как мы приступим к сборке демонстрационного проекта, необходим предпринять некоторые подготовительные действия.

14.2 Установка компилятора для архитектуры RISC-V

Сборка проекта на базе VexRiscv усложнена тем фактом, что помимо генерации и синтезирования аппаратуры нам еще потребуется компилировать программы на языке Си, получать исполняемый машинный код и размещать его в памяти синтезированного микропроцессора. Для того, чтобы компилировать Си программы для архитектуры RISC-V нам потребуется установить один из доступных опенсорсных компиляторов, а именно «GNU C Toolchain for RISC-V». Есть несколько способов установить RISC-V GCC toolchain.

Первый способ — это клонировать репозиторий https://github.com/riscv/riscv-gnu-toolchain и выполнить сборку самостоятельно. В этом случае придется установить ряд дополнительных библиотек и разрешить ряд проблем с зависимостями. Для пользователей ОС Linux и *BSD это не должно являться проблемой. При самостоятельной сборке желательно указать путь для установки компилятора --prefix=/opt/. Рецепт сборки выглядит так:

git clone --recursive https://github.com/riscv/riscv-gnu-toolchain riscv-gnu-toolchain 
cd riscv-gnu-toolchain 

ARCH=rv32im 
rmdir -rf $ARCH 
mkdir $ARCH; cd $ARCH 
../configure  --prefix=/opt/$ARCH --with-arch=$ARCH --with-abi=ilp32 
sudo make -j100500

И ждать два дня.

Второй способ — это скачать готовый (pre-built) вариант компилятора с сайта SiFive, но для этого потребуется пройти регистрацию на сайте компании и указать свой адрес электронной почты. Для скачивания нужно пройти по ссылке:

https://www.sifive.com/software/ => SiFive Freedom SDK for Metal => Download

Скачанный пакет желательно распаковать в каталог /opt/, либо добавить символическую ссылку.

Ну и третий, самый быстрый, способ — установить компилятор GCC из репозитория пакетов дистрибутива вашей ОС, так как с некоторых пор он присутствует почти во всех дистрибутивах ОС Linux и *BSD. Чтобы установить из репозитория:

Для ОС FreeBSD:

$ sudo pkg install riscv64-none-elf-gcc

Для ОС Linux:

$ sudo apt-get install gcc-riscv64-linux-gnu

Для того, чтобы проверить работоспособность компилятора, выполним сборку приложения «hello_world» из репозитория VexRiscv:

rz@devbox:~$ cd VexRiscv/src/main/c/murax/hello_world

rz@devbox:~/VexRiscv/src/main/c/murax/hello_world$ make

mkdir -p build/src/ 
/opt/riscv//bin/riscv64-unknown-elf-gcc -c -march=rv32i  -mabi=ilp32 -DNDEBUG -g -Os -MD -fstrict-volatile-bitfields -fno-strict-aliasing   -o build/src/main.o src/main.c 
/opt/riscv//bin/riscv64-unknown-elf-gcc -S -march=rv32i  -mabi=ilp32 -DNDEBUG -g -Os -MD -fstrict-volatile-bitfields -fno-strict-aliasing   -o build/src/main.o.disasm src/main.c 
mkdir -p build/src/ 
/opt/riscv//bin/riscv64-unknown-elf-gcc -c -march=rv32i  -mabi=ilp32 -DNDEBUG -g -Os -MD -fstrict-volatile-bitfields -fno-strict-aliasing -o build/src/crt.o src/crt.S -D__ASSEMBLY__=1 
/opt/riscv//bin/riscv64-unknown-elf-gcc -march=rv32i  -mabi=ilp32 -DNDEBUG -g -Os -MD -fstrict-volatile-bitfields -fno-strict-aliasing -o build/hello_world.elf build/src/main.o build/src/crt.o -march=rv32i  -mabi=ilp32 -nostdlib -lgcc -mcmodel=medany -nostartfiles -ffreestanding -Wl,-Bstatic,-T,./src/linker.ld,-Map,build/hello_world.map,--print-memory-usage -Lbuild 
Memory region         Used Size  Region Size  %age Used 
             RAM:         896 B         2 KB     43.75% 
/opt/riscv//bin/riscv64-unknown-elf-objcopy -O ihex build/hello_world.elf build/hello_world.hex 
/opt/riscv//bin/riscv64-unknown-elf-objdump -S -d build/hello_world.elf > build/hello_world.asm 
/opt/riscv//bin/riscv64-unknown-elf-objcopy -O verilog build/hello_world.elf build/hello_world.v 

Если сборка не выполняется, то необходимо выяснить путь к компилятору в вашей системе командой which, например:

rz@butterfly:~/VexRiscv/src/main/c/murax/hello_world % which riscv64-none-elf-gcc 
/usr/local/bin/riscv64-none-elf-gcc

и отредактировать ./makefile, заменив значения в переменных RISCV_NAME и RISCV_PATH соответственно показаниям утилиты which:

RISCV_NAME ?= riscv64-none-elf 
RISCV_PATH ?= /usr/local/ 

Замечание для пользователей FreeBSD:

В *BSD системах сборку нужно осуществлять командой gmake (GNU make), а не make (BSD make), так как это две разные несовместимые утилиты. При работе со SpinalHDL я настоятельно рекомендую сделать локальный симлинк для gmake в пользовательский ~/bin/make и установить его первым в поиске, как-то так:

rz@butterfly:~ % ln -s /usr/local/bin/gmake ~/bin/make
rz@butterfly:~ % setenv PATH ~/bin:$PATH 
rz@butterfly:~ % echo $PATH 
/home/rz/bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/home/rz/bin

Такое решение радикально упростит жизнь, так как SpinalHDL постоянно использует GNUтый make. После работы достаточно закрыть сеанс (терминал), чтобы вернуть переменную PATH в исходное состояние и старый BSD make вернется.

14.3 Подготавливаем Makefile, LPF и toplevel.v

Читатель, наверное, уже обратил внимание на то, что в дереве каталогов присутствуют примеры только для отладочных плат на базе ПЛИС Xilinx Artix-7 и Lattice iCE40, нас же интересует сборка для Lattice ECP5 в составе платы «Карно» (ну или любой другой). Поэтому нам необходимо выполнить еще ряд подготовительных действий: создать рабочий подкаталог Karnix для платы «Карно», в котором разместить Makefile для процедуры сборки (компиляция, генерация, синтез, формирование битстрима), LPF файл с описанием сигналов ввода/вывода для нашей платы в соответствии с дизайном который мы собираемся синтезировать и файл с модулем-оберткой toplevel.v для связи внешних сигналов софт-ядра VexRiscv с сигналами на нашей платы.

1. Создадим рабочий каталог:

rz@devbox:~/VexRiscv$ mkdir scripts/Murax/Karnix
rz@devbox:~/VexRiscv$ cd scripts/Murax/Karnix
и перейдем в него:
rz@devbox:~/VexRiscv/scripts/Murax/Karnix$

2. Позаимствуем Makefile из репозитория SpinalTemplateSbtForKarnix и добавим в него вызов сборки Cи программы «hello_world». Готовый Makefile будет выглядеть следующим образом:

Makefile для сборки VexRiscv под плату "Карно"
rz@devbox:~/VexRiscv/scripts/Murax/Karnix$ cat Makefile

NAME = murax_hello_world 
VEXRISCV = ../../../src/main/scala/vexriscv/demo/Murax.scala 
VERILOG = ../../../Murax.v 
BIN = ../../../Murax.v*.bin 
HEX = ../../../src/main/c/murax/hello_world/build/hello_world.hex 
CSRC = ../../../src/main/c/murax/hello_world/src/*.[ch] 
LPF = karnix_cabga256.lpf 
DEVICE = 25k 
PACKAGE = CABGA256 
FTDI_CHANNEL = 0 ### FT2232 has two channels, select 0 for channel A or 1 for channel B 
# 
FLASH_METHOD := $(shell cat flash_method 2> /dev/null) 
UPLOAD_METHOD := $(shell cat upload_method 2> /dev/null) 

all: $(NAME).bit 

compile: $(HEX) 

$(HEX): $(CSRC) 
        (cd ../../../src/main/c/murax/hello_world/; make) 

generate: $(VERILOG) $(BIN) 

$(VERILOG) $(BIN): $(HEX) $(VEXRISCV) 
        (cd ../../..; sbt "runMain vexriscv.demo.Murax_karnix") 

$(NAME).bit: $(LPF) $(VERILOG) toplevel.v 
        yosys -v2 -p "synth_ecp5 -abc2 -top toplevel -json $(NAME).json" $(VERILOG) toplevel.v 
        nextpnr-ecp5 --package $(PACKAGE) --$(DEVICE) --json $(NAME).json --textcfg $(NAME)_out.config --lpf $(LPF) --lpf-allow-unconstrained 
        ecppack --compress --freq 38.8 --input $(NAME)_out.config --bit $(NAME).bit 


upload: 
ifeq ("$(FLASH_METHOD)", "flash") 
        openFPGALoader -v --ftdi-channel $(FTDI_CHANNEL) -f --reset $(NAME).bit 
else 
        openFPGALoader -v --ftdi-channel $(FTDI_CHANNEL) $(NAME).bit 
endif 

clean: 
        -rm *.json *.config *.bit $(VERILOG) $(BIN) $(HEX)

3. Аналогичным образом позаимствуем из репозитория SpinalTemplateSbtForKarnix файл с описанием внешних сигналов karnix_cabga256.lpf и разместим его в этом рабочем каталоге. Здесь нам потребуется добавить сигнальные линии для отладочного UART, для кнопок KEY и для светодиодов LED. Результирующий LPF файл будет выглядеть следующим образом:

karnix_cabga256.lpf
FREQUENCY PORT "io_clk25" 25.0 MHz; 
LOCATE COMP "io_clk25" SITE "B9"; 
IOBUF PORT "io_clk25" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_key[0]" SITE "B13"; 
IOBUF PORT "io_key[0]" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_key[1]" SITE "C13"; 
IOBUF PORT "io_key[1]" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_key[2]" SITE "D13"; 
IOBUF PORT "io_key[2]" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_key[3]" SITE "E13"; 
IOBUF PORT "io_key[3]" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_led[0]" SITE "A13";             # LED0 
IOBUF PORT "io_led[0]" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_led[1]" SITE "A14";             # LED1 - WORK 
IOBUF PORT "io_led[1]" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_led[2]" SITE "A15";             # LED2 - TEST 
IOBUF PORT "io_led[2]" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_led[3]" SITE "B14";             # LED3 
IOBUF PORT "io_led[3]" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_uart_debug_txd" SITE "E12";             # UART_DEBUG_TXD 
IOBUF PORT "io_uart_debug_txd" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_uart_debug_rxd" SITE "D12";             # UART_DEBUG_RXD 
IOBUF PORT "io_uart_debug_rxd" IO_TYPE=LVCMOS33; 
LOCATE COMP "io_rst_n" SITE "R8";               # FPGA_RESET 
IOBUF PORT "io_rst_n" IO_TYPE=LVCMOS33 DRIVE=4;

14.4 Добавляем точку входа для генерации СнК Murax для платы «Карно»

Отредактируем файл ./src/main/scala/vexriscv/demo/Murax.scala, описывающий синтезируемую систему-на-кристалле Murax, и добавим в него точку входа для генерации кода под плату «Карно». Для этого добавим в самый конец файла следующие строки кода на языке SpinalHDL:

object Murax_karnix{ 
  def main(args: Array[String]) { 
    val hex = "src/main/c/murax/hello_world/build/hello_world.hex" 
    SpinalVerilog(Murax(MuraxConfig.default(false).copy(coreFrequency = 25 MHz, onChipRamSize = 96 kB, onChipRamHexFile = hex))) 
  } 
}

Здесь мы описываем «оберточный» объект Murax_karnix из метода main() которого будет вызываться генерация кода Verilog. В качестве параметров в конструктор объекта Murax() можно передать структуру с настройками, среди которых требуется указать частоту тактового генератора на плате (25 МГц), указать объем ОЗУ который потребуется синтезировать (в данном случае 96 КБ) и путь к HEX файлу, содержимое которого будет размещено в это ОЗУ при процедуре конфигурировании микросхемы ПЛИС.

Микросхему Lattice ECP5 25K позволяет синтезировать ОЗУ объемом до 112 КБ из распределенной памяти BRAM, но часть из этой памяти будет задействована под регистры микропроцессора и прочие регистры цифровой аппаратуры, поэтому указываем требуемый размер немного меньше максимального возможного.

14.5 Модифицируем код программы «hello_world»

Код демонстрационной программы «hello_world» располагается в подкаталоге ./src/main/c/murax/hello_world/src/, зайдем в него и осмотримся.

rz@devbox:~/VexRiscv/scripts/Murax/Karnix$ cd ../../../src/main/c/murax/hello_world/src 
rz@devbox:~/VexRiscv/src/main/c/murax/hello_world/src$ ls -l 

-rw-rw-r-- 1 rz rz 1642 Feb  3 19:46 crt.S 
-rw-rw-r-- 1 rz rz  178 Feb  3 19:46 gpio.h 
-rw-rw-r-- 1 rz rz  303 Feb  3 19:46 interrupt.h 
-rw-rw-r-- 1 rz rz 2163 Feb  3 19:49 linker.ld 
-rw-rw-r-- 1 rz rz 1182 Feb  3 21:55 main.c 
-rw-rw-r-- 1 rz rz  483 Feb  3 21:38 murax.h 
-rw-rw-r-- 1 rz rz  217 Feb  3 19:46 prescaler.h 
-rw-rw-r-- 1 rz rz  296 Feb  3 19:46 timer.h 
-rw-rw-r-- 1 rz rz 1263 Feb  3 21:40 uart.h 

Как видно, программа состоит из ряда заголовочных файлов (.h), главного файла main.c с кодом программы, файла crt.S с кодом инициализации «C run-time», написанного на ассемблере и файла конфигурации редактора объектных связей (линковщика) — linker.ld.

Для начала, нам нужно отредактировать файл конфигурации линковщика и указать каким объемом ОЗУ мы располагаем и какого размера стек мы хотим использовать. Откроем файл linker.ld, найдем следующие строки и внесем изменения, выделенные жирным шрифтом:

MEMORY { 
  RAM      (rwx): ORIGIN = 0x80000000, LENGTH = 96k 
} 
_stack_size = DEFINED(_stack_size) ? _stack_size : 2048; 
_heap_size = DEFINED(_heap_size) ? _heap_size : 0; 

Далее нам нужно добавить код инициализации UART в текст программы. По какой-то причине автор программы «hello_world» этого не сделал, видимо понадеявшись на значения по умолчанию для платы Artix, но эти значения не подходят для нашей платы Karnix из-за того, что на нашей плате использован другой тактовый генератор. Откроем файл main.c и отредактируем тело функции main() чтобы оно приняло следующий вид:

main()
void main() { 
        Uart_Config uart_config; 

        uart_config.dataLength = UART_DATA_8; 
        uart_config.parity = UART_PARITY_NONE; 
        uart_config.stop = UART_STOP_ONE; 

        uint32_t rxSamplePerBit = UART_PRE_SAMPLING_SIZE + UART_SAMPLING_SIZE + UART_POST_SAMPLING_SIZE; 

        uart_config.clockDivider = SYSTEM_CLOCK_HZ / UART_BAUD_RATE / rxSamplePerBit - 1; 
        uart_applyConfig(UART, &uart_config); 


        GPIO_A->OUTPUT_ENABLE = 0x0000000F; 
        GPIO_A->OUTPUT = 0x00000001; 
        const int nleds = 4; 
        const int nloops = 1000000; 
        while(1){ 
                println("Hello world, this is VexRiscv!"); 
                for(unsigned int i=0;i<nleds-1;i++){ 
                        GPIO_A->OUTPUT = 1<<i; 
                        delay(nloops); 
                } 
                for(unsigned int i=0;i<nleds-1;i++){ 
                        GPIO_A->OUTPUT = (1<<(nleds-1))>>i; 
                        delay(nloops); 
                } 
        } 
} 

Из приведенного выше текста на языке Си видно, что программа «hello_world» циклически зажигает и гасит четыре светодиода и выдает в порт UART сообщение «Hello world, this is VexRiscv» — всё согласно сложившимся традициям.

Добавим недостающие константы в файл uart.h:

uart.h
#define UART_PRE_SAMPLING_SIZE  1 
#define UART_SAMPLING_SIZE      3 
#define UART_POST_SAMPLING_SIZE 1 
#define UART_BAUD_RATE		115200

#define UART_PARITY_NONE        0 
#define UART_PARITY_EVEN        1 
#define UART_PARITY_ODD         2 

#define UART_STOP_ONE           0 
#define UART_STOP_TWO           1 

#define UART_DATA_5             4 
#define UART_DATA_6             5 
#define UART_DATA_7             6 
#define UART_DATA_8             7 
#define UART_DATA_9             8 

И в файл murax.h:

#define SYSTEM_CLOCK_HZ 25000000 // system clock on Karnix board in Hz

Проверим собираемость программы «hello_world»:

cd .. && make clean && make
rz@devbox:~/VexRiscv/src/main/c/murax/hello_world/src$ cd .. 
rz@devbox:~/VexRiscv/src/main/c/murax/hello_world$ make clean 
rm -rf build/src 
rm -f build/hello_world.elf 
rm -f build/hello_world.hex 
rm -f build/hello_world.map 
rm -f build/hello_world.v 
rm -f build/hello_world.asm 
find build -type f -name '.o' -print0 | xargs -0 -r rm 
find build -type f -name '.d' -print0 | xargs -0 -r rm 
rz@devbox:~/VexRiscv/src/main/c/murax/hello_world$ make 
mkdir -p build/src/ 
/opt/riscv//bin/riscv64-unknown-elf-gcc -c -march=rv32i  -mabi=ilp32 -DNDEBUG -g -Os -MD -fstrict-volatile-bitfields -fno-strict-aliasing   -o build/src/main.o src/main.c 
/opt/riscv//bin/riscv64-unknown-elf-gcc -S -march=rv32i  -mabi=ilp32 -DNDEBUG -g -Os -MD -fstrict-volatile-bitfields -fno-strict-aliasing   -o build/src/main.o.disasm src/main.c 
mkdir -p build/src/ 
/opt/riscv//bin/riscv64-unknown-elf-gcc -c -march=rv32i  -mabi=ilp32 -DNDEBUG -g -Os -MD -fstrict-volatile-bitfields -fno-strict-aliasing -o build/src/crt.o src/crt.S -D__ASSEMBLY__=1 
/opt/riscv//bin/riscv64-unknown-elf-gcc -march=rv32i  -mabi=ilp32 -DNDEBUG -g -Os -MD -fstrict-volatile-bitfields -fno-strict-aliasing -o build/hello_world.elf build/src/main.o build/src/crt.o -march=rv32i  -mabi=ilp32 -nostdlib -lgcc -mcmodel=medany -nostartfiles -ffreestanding -Wl,-Bstatic,-T,./src/linker.ld,-Map,build/hello_world.map,--print-memory-usage -Lbuild 
Memory region         Used Size  Region Size  %age Used 
             RAM:        2756 B        96 KB      2.80% 
/opt/riscv//bin/riscv64-unknown-elf-objcopy -O ihex build/hello_world.elf build/hello_world.hex 
/opt/riscv//bin/riscv64-unknown-elf-objdump -S -d build/hello_world.elf > build/hello_world.asm 
/opt/riscv//bin/riscv64-unknown-elf-objcopy -O verilog build/hello_world.elf build/hello_world.v 

Здесь нас интересует, чтобы компилятор успешно собрал файл build/hello_world.hex и размер области памяти не превышал 96КБ синтезируемых в СнК.

Для тех, кто не хочет тратить время на прохождение всего вышеописанного приключения, приведу ссылку на клон репозитория VexRiscv содержащего ветку с кодом для поддержки платы ПИР СЦХ-254 «Карно»: https://github.com/Fabmicro-LLC/VexRiscvWithKarnix/tree/karnix

14.6 Сборка СнК Murax и ядра VexRiscv

Ну что же, наконец у нас всё готово и можно попытаться запустить сборку СнК на базе ядра VerRiscv. Перейдем в рабочий каталог для нашей платы и запустим make:

rz@devbox:~/VexRiscv/src/main/c/murax/hello_world$ cd ../../../../../scripts/Murax/Karnix 
rz@devbox:~/VexRiscv/scripts/Murax/Karnix$ make

Первым делом мы будем наблюдать как SBT запустит компиляцию кода на языке Scala и SpinalHDL:

(cd ../../..; sbt "runMain vexriscv.demo.Murax_karnix") 
[info] welcome to sbt 1.6.0 (Ubuntu Java 11.0.9.1) 
[info] loading settings for project vexriscv-build from plugins.sbt ... 
[info] loading project definition from /home/rz/VexRiscv/project 
[info] loading settings for project root from build.sbt ... 
[info] set current project to VexRiscv (in build file:/home/rz/VexRiscv/) 
[info] running (fork) vexriscv.demo.Murax_karnix

Компилятор Scala отработал без ошибок и SBT поставил полученный байт-код на исполнение на Java машине:

[info] [Runtime] SpinalHDL v1.10.1    git head : 2527c7c6b0fb0f95e5e1a5722a0be732b364ce43 
[info] [Runtime] JVM max memory : 8294.0MiB 
[info] [Runtime] Current date : 2024.02.04 00:37:10 
[info] [Progress] at 0.000 : Elaborate components 
...
[info] [Progress] at 2.469 : Checks and transforms 
[info] [Progress] at 3.398 : Generate Verilog 
[info] [Warning] 157 signals were pruned. You can call printPruned on the backend report to get more informations. 
[info] [Done] at 4.832 
[success] Total time: 11 s, completed Feb 4, 2024, 12:37:15 AM 

В результате исполнения байт-кода мы получаем набор Verilog файлов. Давайте остановим процесс и посмотрим, что это за файлы:

^C
rz@devbox:~/VexRiscv/scripts/Murax/Karnix$ ls -l ../../../ 
total 1316 
-rw-rw-r-- 1 rz rz   1079 Feb  3 19:46 LICENSE 
-rw-rw-r-- 1 rz rz 351198 Feb  4 11:50 Murax.v 
-rw-rw-r-- 1 rz rz 221184 Feb  4 11:50 Murax.v_toplevel_system_ram_ram_symbol0.bin 
-rw-rw-r-- 1 rz rz 221184 Feb  4 11:50 Murax.v_toplevel_system_ram_ram_symbol1.bin 
-rw-rw-r-- 1 rz rz 221184 Feb  4 11:50 Murax.v_toplevel_system_ram_ram_symbol2.bin 
-rw-rw-r-- 1 rz rz 221184 Feb  4 11:50 Murax.v_toplevel_system_ram_ram_symbol3.bin 
-rw-rw-r-- 1 rz rz  63826 Feb  3 22:13 README.md 
drwxrwxr-x 2 rz rz   4096 Feb  3 19:46 assets 
-rw-rw-r-- 1 rz rz    621 Feb  3 19:46 build.sbt 
-rw-rw-r-- 1 rz rz    998 Feb  3 19:46 build.sc 

Файл Murax.v содержит код нашего СнК и всех его составных частей, в том числе ядро VexRiscv, преобразованное в текст на языке Verilog. Если заглянуть во внутрь этого файла, то мы увидим главный модуль Murax с описанием его внешних сигналов:

module Murax ( 
  input  wire          io_asyncReset, 
  input  wire          io_mainClk, 
  input  wire          io_jtag_tms, 
  input  wire          io_jtag_tdi, 
  output wire          io_jtag_tdo, 
  input  wire          io_jtag_tck, 
  input  wire [31:0]   io_gpioA_read, 
  output wire [31:0]   io_gpioA_write, 
  output wire [31:0]   io_gpioA_writeEnable, 
  output wire          io_uart_txd, 
  input  wire          io_uart_rxd 
);

Файлы с расширением .bin содержат данные для инициализации синтезируемого ОЗУ (RAM) и, по сути, это наша программа «hello_world» в виде последовательности двоичных 8-ми битных слов в текстовом виде. Файлы .bin подключаются внутри файла Murex.v следующими строками кода:

  initial begin 
    $readmemb("Murax.v_toplevel_system_ram_ram_symbol0.bin",ram_symbol0); 
    $readmemb("Murax.v_toplevel_system_ram_ram_symbol1.bin",ram_symbol1); 
    $readmemb("Murax.v_toplevel_system_ram_ram_symbol2.bin",ram_symbol2); 
    $readmemb("Murax.v_toplevel_system_ram_ram_symbol3.bin",ram_symbol3); 
  end 

Данный комплект файлов, вместе с оберточным файлом toplevel.v, передается в утилиту yosys для выполнения синтеза. Еще раз введем команду make и продолжим сборку:

rz@devbox:~/VexRiscv/scripts/Murax/Karnix$ make
yosys -v2 -p "synth_ecp5 -abc2 -top toplevel -json murax_hello_world.json" ../../../Murax.v toplevel.v 
1. Executing Verilog-2005 frontend: ../../../Murax.v 
2. Executing Verilog-2005 frontend: toplevel.v 
…

Здесь мы видим, как на вход утилиты yosys передаются два файла: ../../../Murax.v и toplevel.v, и она начинает их обрабатывать, используя свой Verilog-2005 фронтэнд.

В процессе синтеза, утилита yosys выдаст нам ряд предупреждений о том, что не все биты комплексного сигнала gpio_read привязаны к источникам (драйверам):

3.10. Executing OPT_CLEAN pass (remove unused cells and wires). 
3.11. Executing CHECK pass (checking for obvious problems). 
Warning: Wire toplevel.\murax.system_gpioACtrl.io_gpio_read_buffercc.io_dataIn [31] is used but has no driver. 
Warning: Wire toplevel.\murax.system_gpioACtrl.io_gpio_read_buffercc.io_dataIn [30] is used but has no driver. 
Warning: Wire toplevel.\murax.system_gpioACtrl.io_gpio_read_buffercc.io_dataIn [29] is used but has no driver. 
...
Warning: Wire toplevel.\murax.system_gpioACtrl.io_gpio_read_buffercc.io_dataIn [5] is used but has no driver. 
Warning: Wire toplevel.\murax.system_gpioACtrl.io_gpio_read_buffercc.io_dataIn [4] is used but has no driver. 

Это объясняется тем, что мы задействовали всего первых четыре бита регистра gpioA из 32-х доступных (см файл toplevel.v), которые мы подвязали к линиям кнопок io_key[0]-io_key[3]. Остальные не подвязанные биты будут автоматически подтянуты к логическому «0».

Утилита yosys завершается без ошибок и выдает нам кое-какую статистику о своей деятельности:

3.50. Executing CHECK pass (checking for obvious problems). 
3.51. Executing JSON backend. 
Warnings: 28 unique messages, 28 total 
End of script. Logfile hash: 697ada3929, CPU: user 29.56s system 0.80s, MEM: 151.63 MB peak 
Yosys 0.36+58 (git sha1 ea7818d31, gcc 7.5.0-3ubuntu1~18.04 -fPIC -Os) 
Time spent: 21% 32x opt_clean (7 sec), 21% 11x techmap (6 sec), ... 

Вслед за yosys в конвейере сборки вызывается утилита nextpnr-ecp5. Напомню, что она получает на вход файл с описанием нетлиста в формате JSON, синтезированного утилитой yosys и занимается оптимизацией, размещением и трассировкой синтезированной схемы на базовых элементах целевой микросхемы ПЛИС.

nextpnr-ecp5 --package CABGA256 --25k --json murax_hello_world.json --textcfg murax_hello_world_out.config --lpf karnix_cabga256.lpf --lpf-allow-unconstrained 
Info: constraining clock net 'io_clk25' to 25.00 MHz 

Здесь утилита nextpnr-ecp5 сообщает нам, что она обнаружила сигнал io_clk25, для которого присутствует ограничение по частоте не ниже 25.00 МГц. Это значение далее будет использоваться в процессе STA как целевое.

Далее утилита nextpnr-ecp5 выводит нам статистику о задействованных схемой ресурсах целевой микросхемы ПЛИС до выполнения оптимизации и размещения, т. е. в том виде как это поступило от утилиты yosys:

Info: Logic utilisation before packing: 
Info:     Total LUT4s:      2272/24288     9% 
Info:         logic LUTs:   1818/24288     7% 
Info:         carry LUTs:    238/24288     0% 
Info:           RAM LUTs:    144/ 3036     4% 
Info:          RAMW LUTs:     72/ 6072     1% 
Info:      Total DFFs:      1379/24288     5%

Обратите внимание, что весь СнК вместе с вычислительным ядром занимает всего 1379 D-триггеров и 2272 ячеек LUT-4.

Далее утилита nextpnr-ecp5 сообщает, как произошло размещение внешних сигналов, которые жестко определены в LPF файле:

Info: Packing IOs.. 
Info: pin 'io_uart_debug_txd$tr_io' constrained to Bel 'X58/Y0/PIOB'. 
Info: pin 'io_uart_debug_rxd$tr_io' constrained to Bel 'X58/Y0/PIOA'. 
Info: pin 'io_led[3]$tr_io' constrained to Bel 'X67/Y0/PIOA'. 
Info: pin 'io_led[2]$tr_io' constrained to Bel 'X67/Y0/PIOB'. 
Info: pin 'io_led[1]$tr_io' constrained to Bel 'X65/Y0/PIOB'. 
Info: pin 'io_led[0]$tr_io' constrained to Bel 'X65/Y0/PIOA'. 
Info: pin 'io_key[3]$tr_io' constrained to Bel 'X62/Y0/PIOB'. 
Info: pin 'io_key[2]$tr_io' constrained to Bel 'X62/Y0/PIOA'. 
Info: pin 'io_key[1]$tr_io' constrained to Bel 'X60/Y0/PIOB'. 
Info: pin 'io_key[0]$tr_io' constrained to Bel 'X60/Y0/PIOA'. 
Info: pin 'io_core_jtag_tms$tr_io' constrained to Bel 'X0/Y23/PIOB'. 
Info: pin 'io_core_jtag_tdo$tr_io' constrained to Bel 'X0/Y23/PIOD'. 
Info: pin 'io_core_jtag_tdi$tr_io' constrained to Bel 'X0/Y23/PIOC'. 
Info: pin 'io_core_jtag_tck$tr_io' constrained to Bel 'X0/Y23/PIOA'. 
Info: pin 'io_clk25$tr_io' constrained to Bel 'X38/Y0/PIOA'. 

После чего утилита выполняет упаковку остальных сигналов схемы:

Info: Packing constants.. 
Info: Packing carries... 
Info: Packing LUTs... 
Info: Packing LUT5-7s... 
Info: Packing FFs... 
Info:     542 FFs paired with LUTs. 
Info: Generating derived timing constraints... 

В процессе упаковки обнаруживаются сигналы тактирования, которые утилита nextpnr-ecp5 предлагает разместить в глобальных линиях:

Info: Promoting globals... 
Info:     promoting clock net io_clk25$TRELLIS_IO_IN to global network 
Info:     promoting clock net io_core_jtag_tck$TRELLIS_IO_IN to global network 

После размещения утилита nextpnr-ecp5 еще раз выдает статистику задействованных ресурсов микросхемы ПЛИС, но уже в более детальном виде:

Info: Device utilisation: 
Info:             TRELLIS_IO:    15/  197     7% 
Info:                   DCCA:     2/   56     3% 
Info:                 DP16KD:    48/   56    85% 
Info:             MULT18X18D:     0/   28     0% 
Info:                 ALU54B:     0/   14     0% 
Info:                EHXPLLL:     0/    2     0% 
Info:                EXTREFB:     0/    1     0% 
Info:                   DCUA:     0/    1     0% 
Info:              PCSCLKDIV:     0/    2     0% 
Info:                IOLOGIC:     0/  128     0% 
Info:               SIOLOGIC:     0/   69     0% 
Info:                    GSR:     0/    1     0% 
Info:                  JTAGG:     0/    1     0% 
Info:                   OSCG:     0/    1     0% 
Info:                  SEDGA:     0/    1     0% 
Info:                    DTR:     0/    1     0% 
Info:                USRMCLK:     0/    1     0% 
Info:                CLKDIVF:     0/    4     0% 
Info:              ECLKSYNCB:     0/   10     0% 
Info:                DLLDELD:     0/    8     0% 
Info:                 DDRDLL:     0/    4     0% 
Info:                DQSBUFM:     0/    8     0% 
Info:        TRELLIS_ECLKBUF:     0/    8     0%
Info:           ECLKBRIDGECS:     0/    2     0% 
Info:                   DCSC:     0/    2     0% 
Info:             TRELLIS_FF:  1379/24288     5% 
Info:           TRELLIS_COMB:  2346/24288     9% 
Info:           TRELLIS_RAMW:    36/ 3036     1%

Здесь DCCA — это число глобальных линий для тактовых сигналов, DP16KD — число блоков распределенной памяти (каждый по 16Кбит), TRELLIS_FF — число D-триггеров, а TRELLIS_COMB — число логических (комбинационных) элементов LUT-4. Важно понимать, что данные аппаратные блоки специфичны для микросхемы ПЛИС Lattice ECP5, для других микросхем названия блоков и их количество будет иным.

Утилита nextpnr-ecp5 производит расчет максимальных задержек для тактовых сигналов и выдает нам следующую информацию:

Info: Max frequency for clock         '$glbnet$io_clk25$TRELLIS_IO_IN': 49.99 MHz (PASS at 25.00 MHz) 
Info: Max frequency for clock '$glbnet$io_core_jtag_tck$TRELLIS_IO_IN': 142.92 MHz (PASS at 12.00 MHz) 

Далее запускается процесс оптимизации и трассировки, который занимает достаточно большое время и требователен к вычислительным ресурса системы, на которой происходит сборка.

Info: Routing globals... 
...
Info: Critical path report for clock '$glbnet$io_clk25$TRELLIS_IO_IN' (posedge -> posedge): 

По окончанию процесса трассировки утилита nextpnr-ecp5 еще раз вычисляет задержки и выдает нам полученную информацию:

Info: Max frequency for clock         '$glbnet$io_clk25$TRELLIS_IO_IN': 69.80 MHz (PASS at 25.00 MHz) 
Info: Max frequency for clock '$glbnet$io_core_jtag_tck$TRELLIS_IO_IN': 149.30 MHz (PASS at 12.00 MHz) 

Работа утилиты nextpnr-ecp5 завершается сообщением вида:

2 warnings, 0 errors 
Info: Program finished normally.

После плэйсера nextpnr-ecp5 по цепочке запускается утилита ecppack, которая быстро и немногословно формирует битстрим для микросхемы ПЛИС Lattice ECP5:

ecppack --compress --freq 38.8 --input murax_hello_world_out.config --bit murax_hello_world.bit

Процесс сборки окончен, результирующий битстрим находится в файле murax_hello_world.bit в текущем рабочем каталоге:

rz@devbox:~/VexRiscv/scripts/Murax/Karnix$ ls -l 
total 8120 
-rw-rw-r-- 1 rz rz    1345 Feb  3 22:08 Makefile 
-rw-rw-r-- 1 rz rz   11486 Feb  3 19:47 karnix_cabga256.lpf 
-rw-rw-r-- 1 rz rz  257823 Feb  4 13:05 murax_hello_world.bit 
-rw-rw-r-- 1 rz rz 6246585 Feb  4 13:05 murax_hello_world.json 
-rw-rw-r-- 1 rz rz 1785563 Feb  4 13:05 murax_hello_world_out.config 
-rw-rw-r-- 1 rz rz    1158 Feb  3 21:51 toplevel.v 

14.7 Запускаем СнК Murax на ПЛИС и тестируем «hello_world»

Перед тем как мы подключим плату «Карно» и запустим прошивку, нам необходимо установить еще одну утилиту — эмулятор терминала, чтобы мы могли наблюдать за тем, какие данные передаются в отладочный порт UART. Напомню, что программа «hello_world» передает в этот порт строку символов приветствия.

В качестве эмулятора терминала предлагаю использовать утилиту minicom. Она присутствует во всех известных дистрибутивах ОС Linux и FreeBSD, поэтому установить её совсем не сложно:

Для FreeBSD:

rz@butterfly:~ % sudo pkg install minicom

Для ОС Linux:

rz@devbox:~$ sudo apt-get install minicom

Напомню, что на плате «Карно» присутствуют два последовательных порта доступных через USB. Порт 0 работает в режиме JTAG и используется для прошивки NOR flash и микросхемы ПЛИС. Порт 1 является отладочным и может быть использован пользователем по своему усмотрению. Программа «hello_world» будет выдавать строку приветствия именно в этот порт со скоростью 115200 бод без аппаратного контроля (RTS/CTS Flow Control OFF) и стандартного формат фрейма 8N1.

Подключим плату «Карно» к рабочему компьютеру, откроем на нем еще одно окно терминала и запустим в нем утилиту minicom:

Для FreeBSD:

rz@butterfly:~ % sudo minicom -b 115200 -D /dev/ttyU1

Для ОС Linux:

rz@devbox:~$ sudo minicom -b 115200 -D /dev/ttyUSB1

Зайдем в настройки нажав Ctrl-A+O, зайдем в меню «Serial port setup» и отключим RTS/CTS нажатием клавиши «F»:

F - Hardware Flow Control : No

Через меню «Save setup as dfl» сохраним текущие настройки как настройки по умолчанию.

Теперь можно запустить загрузку битстрима в ПЛИС командой make upload. В процессе загрузки битстрима утилита openFPGALoader будет сообщать о ходе выполнения:

rz@devbox:~/VexRiscv/scripts/Murax/Karnix$ make upload

Jtag frequency : requested 6.00MHz   -> real 6.00MHz   
Open file DONE 
Parse file DONE 
Enable configuration: DONE 
SRAM erase: DONE 
Detail: 
Jedec ID          : ef 
memory type       : 70 
memory capacity   : 18 
flash chip unknown: use basic protection detection 
Erasing: [==================================================] 100.00% 
Done 
Writing: [===================================================] 100.00% 
Done 
Refresh: DONE 

Сообщение DONE означает успешное завершение. Если вместо этого наблюдается сообщение типа:

write to flash 
No cable or board specified: using direct ft2232 interface 
unable to open ftdi device: -3 (device not found) 

это означает, что утилита openFPGALoader не смогла выполнить подключение по USB к программатору. Наиболее вероятная причина такого исхода — отсутствие прав доступа к устройству. Проверьте подключение кабеля и попробуйте запустить процесс загрузки от пользователя root.

Если процесс завершился успешно, то на плате «Карно» мы сразу будем наблюдать, как один за другим по цепочке зажигаются и гаснут светодиоды, а в окне терминала minicom будем наблюдать сообщения приветствия:

Welcome to minicom 2.8 
OPTIONS: I18n 
Compiled on Jan 29 2024, 16:10:11. 
Port /dev/ttyU1, 03:09:59 
Press CTRL-A Z for help on special keys                                          
                                                                                 
Hello world, this is VexRiscv!                                              
Hello world, this is VexRiscv!                                              
Hello world, this is VexRiscv! 
Hello world, this is VexRiscv!

Для завершение сессии в эмуляторе терминала minicom следует нажать последовательность: Ctrl-A, X, Enter.

= = = ПРОДОЛЖЕНИЕ СЛЕДУЕТ = = =

PS: Ссылку на статью в формате PDF выложу после публикации второй части.

Комментарии (27)


  1. YuriPanchul
    21.03.2024 23:12
    +10

    Монументально! Буду на следующей неделе читать, извлекать что не знаю


    1. orbion
      21.03.2024 23:12
      +2

      При экспорте в PDF — 132 страницы. И это только первая часть...

      Автору — респект!


      1. checkpoint Автор
        21.03.2024 23:12
        +2

        Я рад, что Вам понравилось. :)


  1. CodeRush
    21.03.2024 23:12
    +4

    Вот это талмуд, мое почтение. Программировал когда-то давно на VHDL и SystemC в магистратуре, и с тех пор остались положительные впечатления от первого и не очень положительные от второго.

    В связи с Yosys рекомендую обратить внимание на проект Glasgow Interface Explorer и другие проекты уважаемой WQ.


  1. 0x1A4
    21.03.2024 23:12
    +1

    Спасибо, интересно. Пару раз пробовал погрузиться в тему ПЛИС, но получал больше вопросов, чем ответов. А тут написано гораздо понятнее, чем вводные мануалы, что мне попадались.

    Подскажите, имеет ли смысл начинать знакомство с ПЛИС сразу со SpinalHDL, или лучше сначала разобраться с Verilog или VHDL. Синтаксис Verilog на первый взгляд показался приятным.


    1. checkpoint Автор
      21.03.2024 23:12
      +2

      Можно конечно начать сразу со SpinalHDL, но в какой-то момент знания Verilog-а всё равно Вам потребуются. Без него в этой теме всё еще никак не обойтись. Поэтому, рекомендую все же начать с basics-graphics-music - это очень простые обучающие примеры, с ними легко постичь основы цифровой схематехники и синтеза. Выполнить первые 7-10 лаб, проникнуться темой и потом попробовать всё то же самое на SpinalHDL.

      SpinalHDL становится интересен тогда, когда в Вашем проекте появляются сложные сущности - вычислительные ядра, контроллеры различной аппаратуры и т.д.


  1. ahdenchik
    21.03.2024 23:12

    Во-вторых, весь код комбинационной и последовательностной логики описывается в одном блоке под одним условным оператором when(). Я уверен, что для большинства разработчиков, давно пишущих код на Verilog и/или VHDL, такое решение будет просто шокирующим. На SpinalHDL такой стиль является нормой, и он позволяет существенно сократить число строк кода, а сам код сделать более понятным и читаемым.

    Иногда таки нужно разделять блокирующие и неблокирующие присваивания, чтобы не наплодить случайно лишних D-триггеров


    1. checkpoint Автор
      21.03.2024 23:12

      SpinalHDL не позволит Вам сделать лишнего. Если переменная обьявлена как сигнал, то сделать из неё триггер случайным образом не выйдет. И наоборот.


  1. radiolok
    21.03.2024 23:12
    +4

    Жду продолжения! Очень активно использую iverilog+verilator+yosys для разработки своего лампового компьютера :) И стараюсь как можно больше о них рассказывать, ведь чем больше таких статей хороших и разных - тем активнее развиваются тулы.

    Не совсем понятна суть заголовка - почему "нетрадиционный" метод разработки? Вполне себе традиционный, а вот инструменты специфические. Качество синтеза того же yosys пока оставляет желать лучшего. Впрочем после обновления до v0.37 он приятно удивил.

    Еще бы нормальный визуализатор RTL или нетлиста найти, а то что по дефолту из dot-файла yosys выходит - критики не выдерживает.


    1. checkpoint Автор
      21.03.2024 23:12

      Есть два варианта: netlistsvg и xdot. Оба очень посредственные. Надо писать своё. Желательно, чтобы еще входной Verilog умел читать и извлекать правильные имена сигналов.


  1. unreal_undead2
    21.03.2024 23:12

    Далее я перечислю список известных производителей микросхем ПЛИС, некоторые из популярных моделей, а также среды разработки (тулы).

    Все, конечно, поняли, что следующий абзац про Xilinx/AMD, но можно было бы и явно написать )


  1. unreal_undead2
    21.03.2024 23:12

    Для того, чтобы компилировать Си программы для архитектуры RISC-V нам потребуется установить один из доступных опенсорсных компиляторов, а именно «GNU C Toolchain for RISC-V».

    Я игрался с llvm/clang - там поддержка RISC V в бэкенде появляется вместе с другими архитектурами при стандартном cmake;make , дальше надо только явно собрать компиляторный рантайм.


  1. nerudo
    21.03.2024 23:12

    Когда автор то ли сам не понимает, то ли намеренно лжетвводит в заблуждение в самом начале, как-то читать дальше становится не интересно. Verilog действительно более лаконичен, но приведенные примеры на рис 4-5 используют разные методики описания, сравнивать которые по объему бессмысленно.


    1. DmitryZlobec
      21.03.2024 23:12

      Дело вообще не в Verilog versus что-то там. Порой путь OpenSource приводит нас к очень странным результатам.
      Yosys как стандарт на вход поддерживает только Verilog, есть конечно сахарок от SystemVerilog, но не как стандарт IEEE 1800-2023. Поэтому в маршруте проектирования нет особой разницы на чем вы разрабатываете дизайн: SystemVerilog, SpinalHDL, или Аmaranth, это все равно все будет преобразовано в Verilog и потом уже синтезировано на ПЛИС или ASIC. Поэтому все, что кажется странным и бесполезным для Enterprise в случае с Yosys является логичным и понятным.


    1. checkpoint Автор
      21.03.2024 23:12

      Приведите пожалуйста свой пример 4-х битового сумматора на VHDL. Я посмотрю и может быть заменю в статье.


      1. nerudo
        21.03.2024 23:12
        +1

        Видимо как-то так:

        use ieee.numeric_std.all;

        ...

        architecture rtl of Ripple_Adder is

        signal sum : unsigned(4 downto 0);

        begin

        sum <= unsigned('0' & A) + unsigned('0' & B) + unsigned("0000" & Cin);

        S <= std_logic_vector(sum(3 downto 0));

        Cout <= sum(4);

        end rtl;

        Если не нравятся кастинги - можно

        1. использовать пакет numeric_std_unsigned, но лично я его идейно не принял;

        2. std_logic_vector в entity заменить на unsigned, но это вопрос согласования типов интерфейсов в проекте.


        1. checkpoint Автор
          21.03.2024 23:12

          Спасибо. Заменил на Ваш вариант. Кода получилось не на много меньше. :)


  1. nv13
    21.03.2024 23:12

    "Ничтожно низкая стоимость ошибки" - может это всё таки только желаемое, с учётом даже не до конца описанных технологий?)) Да и в обычном программировании эта низкая стоимость наблюдается только в бесплатных сервисах и приложениях, где, по большому счёту, на пользователя наплевать и необходимость немедленно устранять ошибку отсутствует.

    Если плис используется в более менее сложной массовой аппаратуре, то даже на тестирование этой аппаратуры уходят существенные ресурсы - автоматизация, симуляция, помещение, стойки, логи, люди. Ошибка требует исправления, верификации, и, как правило, полного цикла тестирования перед выводом в релиз, это меняет графики текущей разработки, сдвигает сроки.

    Всё таки ниша FPGA, на мой взгляд, не очень изменилась за 25-30 лет - малосерийное и специальное применение там, где неэффективно использовать ASIC и готовый контроллер-чип.

    Но в остальном очень интересная и информативная статья, 20 лет назад ничего подобного не проприентарного для полного цикла проектирования не было.


    1. Brak0del
      21.03.2024 23:12

      Если плис используется в более менее сложной массовой аппаратуре, то даже на тестирование этой аппаратуры уходят существенные ресурсы - автоматизация, симуляция, помещение, стойки, логи, люди. Ошибка требует исправления, верификации, и, как правило, полного цикла тестирования перед выводом в релиз, это меняет графики текущей разработки, сдвигает сроки.

      И всё же цена ошибки получается на порядки меньше цены ошибки для ASIC-ов, не правда ли? Ведь не требуется делать респин, выкладывая за это очередные миллионы долларов и месяца времени ожидания. Время обратной связи, имхо, критический фактор, в случае FPGA респин занимает от пары минут до нескольких часов. NRE существенно меньше в случае FPGA даже для сложной массовой аппаратуры, вот пример, где Microsoft делится своими восторгами по поводу скорости разработки хардвера, которая была у них при использовании FPGA. Их начинку Azure-карточек на всех стадиях делала команда из 5 FPGA-шников и горстки программистов.


      1. unreal_undead2
        21.03.2024 23:12

        Кстати, не вижу публикаций от проекта Catapult после 2018го - наигрались и прикрыли или просто отдали в production и исследовать больше нечего?


        1. Brak0del
          21.03.2024 23:12
          +1

          Насколько понял, Catapult где-то тогда разделился на ветку ускорителей ИИ Brainwave и ветку прочих ускорителей для Azure. Brainwave что-то исследовали, поначалу что-то получалось, но сейчас уже всё, уступили место GPU. В Azure ускорители ушли зарабатывать деньги в продакшн, поделившись ещё на какие-то ветки. Одна, Azure Boost примерно соответствует функциональности Catapult и здравствует до сих пор, но про исследования не слышно.


      1. nv13
        21.03.2024 23:12

        Разговор про ничтожную стоимость ошибки, как в программировании. Я участвовал в одном проекте лет 20 назад - да, можно быстро править алгоритмы, да, можно на ходу использовать отладочные прошивки, да, разрешённой альтернативы у нас этим 8 stratix-ам вообще не было. Но вот издержки должны учитываться и не следует убеждать начальство, что они ничтожны)) Майкрософт просто отлаживался на бесплатных пользователях не неся никакой ответственности, а попробуйте крупному корпоративному клиенту продать что то необкатанное, а потом ещё быстро не починить. По мне так эта фраза является очень вредной как в обычном программировании, так и в технологиях вокруг плис, которые, как следует из статьи, становятся всё ближе к разработчику без пары десятков тысяч на вход)


        1. Brak0del
          21.03.2024 23:12
          +1

          Разговор про ничтожную стоимость ошибки, как в программировании. 

          Но вот издержки должны учитываться и не следует убеждать начальство, что они ничтожны))

          Также должны учитываться издержки на 1 итерацию и скорость итераций, как быстро может быть получена ценность. FPGA по этим критериям позволяют приблизиться к гибким методологиям в софте, получать рабочие промежуточные результаты, адаптироваться к изменениям в требованиях и тестировать в приближенных к реальным условиях. Частые итерации позволяют снизить стоимость ошибки и уменьшить требования к кадрам.


          1. nv13
            21.03.2024 23:12
            +1

            Я понимаю о чём Вы говорите, но Вам сильно повезёт или уже повезло работать в таких условиях.


  1. checkpoint Автор
    21.03.2024 23:12
    +1

    Вы говорите про очень тяжелый вариант использования ПЛИС - как относительно дешевый вариант замены ASIC. Да, там затраты на разработку будут сопоставимы с разработкой микросхемы. Я же вещаю про вариант "ПЛИС вместо МК". Во второй части будет про это.


  1. sergebn
    21.03.2024 23:12

    Вопрос. Я выполняю make и получаю ошибку

    $ make
    mkdir -p build/src/
    /usr/bin/riscv64-unknown-elf-gcc -c -march=rv32i -mabi=ilp32 -DNDEBUG -g -Os -MD -fstrict-volatile-bitfields -fno-strict-aliasing -o build/src/main.o src/main.c
    In file included from src/main.c:2:
    /usr/lib/gcc/riscv64-unknown-elf/10.2.0/include/stdint.h:9:16: fatal error: stdint.h: No such file or directory
    9 | # include_next <stdint.h>
    | ^~~~~~~~~~
    compilation terminated.
    make: *** [makefile:111: build/src/main.o] Error 1

    Какой stdint.h ищет если уже stdint.h найден и открыт?


    1. checkpoint Автор
      21.03.2024 23:12

      Беда с тими новыми компиляторами. Вот тут человек нашел решение:

      At the end,

      i resolved the problem by adding to KBUILD_CFLAGS += $(call cc-option,-ffreestanding)

      That restricts gcc use standard stdint stdint-gcc.h

      То есть нужна опция -ffreestanding в CFLAGS.