Долгое время я мечтал научиться работать с FPGA, присматривался. Потом купил отладочную плату, написал пару hello world-ов и положил плату в ящик, поскольку было непонятно, что с ней делать. Потом пришла идея: а давайте напишем генератор композитного видеосигнала для древнего ЭЛТ-телевизора. Идея, конечно, забавная, но я же Verilog толком не знаю, а так его ещё и вспоминать придётся, да и не настолько этот генератор мне нужен… И вот недавно захотелось посмотреть в сторону RISC-V софт-процессоров. Нужно с чего-то начать, а код Rocket Chip (это одна из реализаций) написан на Chisel — это такой DSL для Scala. Тут я внезапно вспомнил, что два года профессионально разрабатываю на Scala и понял: время пришло... Так что, если хотите почитать историю из жизни кусачек, цифрового мультиметра и осциллографа, который осознал себя, то добро пожаловать под кат. Итак, что же будет в этой статье? В ней я опишу свои попытки генерации композитного PAL-видеосигнала (почему PAL? — просто мне попался хороший tutorial именно по генерации PAL) на плате Марсоход 2 за авторством nckma. Про RISC-V в этой статье я вообще ничего не скажу. :) Для начала немного о Scala и Chisel: Scala — это язык, работающий поверх Java Virtual Machine и прозрачно использующий существующие Java-библиотеки (хотя также есть Scala.js и Scala Native). Когда я только начал его изучать, у меня сложилось ощущение, что это такой весьма жизнеспособный гибрид "плюсов" и Хаскеля (впрочем, коллеги не разделяют этого мнения) — уж больно продвинутая система типов и лаконичный язык, но из-за необходимости скрестить функциональщину с ООП обилие языковых конструкций местами навевало воспоминания о C++. Впрочем, не надо бояться Scala — это очень лаконичный и безопасный язык с мощной системой типов, на котором поначалу можно просто писать как на улучшенной Java. А ещё, насколько мне известно, Scala изначально разрабатывалась как язык для удобного создания Domain Specific Languages — это когда описываешь, скажем, цифровую аппаратуру или ноты на формальном языке, и этот язык выглядит вполне логично с точки зрения своей предметной области. А потом ты вдруг узнаёшь, что это был корректный код на Scala (ну, или Haskell) — просто добрые люди написали библиотечку с удобным интерфейсом. Chisel — это как раз такая библиотека для Scala, которая позволяет на удобном DSL описать цифровую логику, а потом запустить полученный Scala-код и сгенерировать код на Verilog (или ещё чём-нибудь), который можно будет скопировать в проект Quartus-а. Ну или сразу запустить стандартные scala-style unit-тесты, которые сами просимулируют тестбенчи и выдадут отчёт о результатах. Для знакомства с цифровой схемотехникой очень рекомендую вот эту книгу (она уже есть и в печатном русскоязычном варианте). На самом деле, моё планомерное знакомство с миром FPGA почти заканчивается на этой книге, поэтому конструктивная критика в комментариях приветствуется (впрочем, повторюсь, книга чудесная: рассказывает от азов и до создания простенького конвееризованного процессора. А ещё там есть картинки ;) ). Ну а по Chisel есть неплохой официальный tutorial. Disclaimer: автор не несёт ответственности за погоревшую аппаратуру, и если надумаете повторять эксперимент — лучше проверьте уровни сигналов осциллографом, переделайте аналоговую часть и т.д. И вообще — соблюдайте технику безопасности. (Я вот, например, в процессе написания статьи осознал, что ноги — это тоже конечности, и нечего их совать в батарею центрального отопления, держась рукой за вывод платы...) Кстати, эта зараза ещё и помехи на телевизор в соседней комнате давала по ходу отладки... Писать код мы будем в IntelliJ Idea Community Edition, в качестве системы сборки будет sbt, поэтому создадим каталог, положим туда .gitignore, project/build.properties, project/plugins.sbt отсюда и Теперь откроем это в Идее и попросим импортировать sbt-проект — при этом sbt скачает необходимые зависимости. Для начала давайте попробуем написать простенький ШИМ. Логика у меня была примерно следующая: чтобы сгенерировать сигнал коэффициента заполнения n/m, изначально положим в регистр 0 и будем прибавлять к нему по n каждый шаг. Когда значение регистра превысит m — вычтем m и выдадим высокий уровень на один такт. Вообще-то, оно будет глючить, если n > m, но будем считать это неопределённым поведением, которое нужно для оптимизации реально используемых случаев. Не буду пересказывать весь beginner's guide — он читается за пол-часа, скажу лишь, что для того, чтобы описать модуль, нам нужно импортировать chisel3._ и отнаследоваться от абстрактного класса Module. Абстрактный он потому, что нам нужно описать Bundle под названием io — в нём будет весь интерфейс модуля. При этом у нас неявно появятся входы clock и reset — отдельно их описывать не нужно. Вот, что получилось: Заметили, мы вызываем метод .W у обычного инта, чтобы получить ширину порта, а метод .asUInt(width.W) мы вообще вызываем у целочисленного литерала! Как такое возможно? — ну, в Smalltalk мы бы просто определили новый метод у класса Integer (или как он там называется), но в JVM у нас всё-таки не всё объект — есть ещё и примитивные типы, и Scala это понимает (и, кроме того, есть сторонние классы, которые мы не можем изменять). Поэтому есть разнообразные implicit-ы: в данном случае Scala, вероятно, находит что-то вроде в текущей области видимости, поэтому у обычного инта появляются сверхспособности. Вот одна из особенностей, делающая Scala более лаконичной и удобной для создания DSL. PeekPokeTester — это один из трёх стандартных тестеров в Chisel. Он позволяет выставлять значения на входах DUT (device under test) и проверять значения на выходах. Как мы видим, для тестов используется обычный ScalaTest и тесты занимают места в 5 раз больше самой реализации, что, в принципе, и для софта нормально. Впрочем, подозреваю, что бывалые разработчики аппаратуры, "отливаемой в кремнии", лишь улыбнутся с такого микроскопического количества тестов. Запускаем и упс... Ага, поправим в PWM в строчке io.pulse := nextValue > io.denominator знак на >=, перезапустим тесты — всё работает! Боюсь, тут бывалые разработчики цифровой аппаратуры захотят меня убить за столь легкомысленное отношение к проектированию (и некоторые разработчики софта к ним с радостью присоединятся)... Также нам понадобится генератор, который будет выдавать импульсы синхронизации для "полукадров". Почему "полу-"? потому что сначала передаются нечетные строки, потом чётные (ну, или наоборот, но нам сейчас не до жиру). При снятии сигнала reset он выстреливает прямоугольными импульсами с длинами промежутков между переключениями, заданными параметром lengths, после чего навечно остаётся в последнем состоянии. Этот пример демонстрирует использование таблиц значений с помощью VecInit, а также способ получения необходимой ширины регистра: chisel3.util.log2Ceil(maxVal + 1).W. Не помню, честно говоря, как оно в Verilog сделано, но в Chisel для создания такого параметризованного вектором значений модуля достаточно вызвать конструктор класса с нужным параметром. Вы, наверное, спросите: «Если входы clock и reset генерируются неявно, то как мы будем на каждый кадр "перезаряжать" генератор импульсов?» Разработчики Chisel всё предусмотрели: Для того, чтобы телевизор хоть как-то нас понял, нужно поддержать "протокол" среднего уровня хитрости: есть три важных уровня сигнала: Почему 0В я назвал специальным? Потому что при плавном переходе от 0.3В к 1.0В мы плавно переходим от чёрного к белому, а между 0В и 0.3В, насколько я сумел понять, нет никаких промежуточных уровней и используется 0В только для синхронизации. (На самом деле, оно изменяется даже не в диапазоне 0В — 1В, а -0.3В — 0.7В, но, будем надеяться, на входе всё равно стоит конденсатор) Как учит нас эта замечательная статья, композитный PAL-сигнал состоит из нескончаемого потока из повторяющихся 625 строк: большинство из них представляют собой строки, собственно, картинки (отдельно чётные и отдельно нечётные), некоторые используются для целей синхронизации (для них мы и делали генератор сигналов), некоторые на экране не видны. Выглядят они так (не буду пиратствовать и дам ссылки на оригинал): Попробуем описать интерфейсы модулей: BWGenerator будет управлять таймингами и т.д., ему нужно знать, на какой частоте он работает: PalColorCalculator будет рассчитывать уровень сигнала яркости, а также дополнительный сигнал цветности: В модуле PalGenerator просто перекоммутируем два указанных модуля: Это всё хорошо, но мы хотим зашить полученный дизайн в плату. Для этого надо синтезировать Verilog. Делается это весьма нехитрым образом: Собственно, в двухстрочном методе main() мы это делаем два раза, весь остальной код — это ещё один модуль, который прилепляет рядом Теперь нужно это запихнуть в проект Quartus. Для Марсохода 2 нам понадобится бесплатная версия Quartus 13.1. Как его установить, написано на сайте Марсоходов. Оттуда же я скачал "Первый проект" для платы Марсоход 2, положил его в репозиторий и немного поправил. Поскольку я не электронщик (да и FPGA меня на самом деле больше интересуют как ускорители, чем как платы интерфейсов), то Сидит программист глубоко в отладке.
Подходит сынишка:
 — Папа, почему солнышко каждый день встает на востоке, а садится на западе?
 — Ты это проверял?
 — Проверял.
 — Хорошо проверял?
 — Хорошо.
 — Работает?
 — Работает.
 — Каждый день работает?
 — Да, каждый день.
 — Тогда ради бога, сынок, ничего не трогай, ничего не меняй. … я просто удалил генератор VGA-сигнала и добавил свой модуль.  После этого я подключил аналоговый ТВ-тюнер к другому компьютеру (ноутбуку), чтобы была хоть какая-то гальваническая развязка между питанием генератора и потребителя сигналов и просто подал сигнал с пинов IO7 (+) и GND (-) платы на композитный вход (минус на наружный контакт, плюс — в центр). Ну, то есть как "просто"… Просто было бы, если бы руки откуда надо росли, ну или если бы у меня были соединительные провода female-male. Но у меня есть только связка male-male проводов. Зато у меня есть упоротость и кусачки! В общем, запоров один провод, я таки сделал себе два почти рабочих — с трудом, но цепляющихся к плате. И вот, что увидел:  На самом деле, я вас, конечно, немного обманул. Показанный выше код у меня получился после где-то трёх часов отладки "на железе", но, блин, я его написал, и оно работает!!! И, учитывая, что раньше с серьёзной электроникой я был почти не знаком, считаю, что задача оказалась не жуть, какая сложная. Ну, что же, дело осталось за малым — дописать генератор цветного видеосигнала. Я взял туториал и начал пытаться формировать color burst (прибавленная к уровню чёрного цвета синусоида на несущей частоте цветового сигнала, на небольшое время выдаваемая во время HSync) и, собственно, цветовой сигнал по формуле. Но вот не выходит, хоть ты тресни… В какой-то момент до меня дошло, что, несмотря на то, что частота при беглом взгляде в документ в глаза не бросалась, телевизор едва ли подстроится под произвольную. Поискав, я нашёл, что в PAL используется частота несущей 4.43 МГц. "Дело в шляпе" — подумал я. "Хрен тебе" — ответил тюнер. Спустя целый день отладки и всего один раз увидев проблески цвета на картинке (причём, когда сказал тюнеру, что это вообще NTSC) 
вспоминается.... Тут я понял, что без осциллографа мне не обойтись. А, как я уже говорил, с электроникой я знаком плохо, и такого чуда техники у меня, естественно, дома не водится. Покупать? Дороговато для одного эксперимента… А из чего его можно соорудить на коленке? Подключить сигнал на линейный вход звуковой карты? Ага, 4 с половиной мегагерца — едва ли заведётся (по крайней мере без переделки). Хм, а ведь у Марсохода есть АЦП на 20 МГц, но вот передавать в компьютер сырой поток скорости последовательного интерфейса не хватит. Ну, где-то всё равно придётся обрабатывать сигнал для вывода на экран, и фактически битов информации там будет вполне приемлемое количество, но это же ещё с последовательным портом возиться, программы для компьютера писать… Тут-то мне и подумалось, что инженер должен развивать в себе здоровую упоротость: есть неработающий формирователь цветного изображения, есть АЦП… Но чёрно-белое-то изображение выводится стабильно… Ну так пусть генератор сигнала сам себя и отлаживает! Лирическое отступление (как говорится, «Мнение студента не обязано совпадать с мнением преподавателя, здравым смыслом и аксиоматикой Пеано»): Когда я добавил генерацию цвета со всякими там умножениями и прочими сложными вещами, сильно просела Fmax для формирователя сигнала. Что же такое Fmax? Насколько я это понял из учебника Harris&Harris, САПР для FPGA предпочитает, когда на Verilog пишут не абы как в пределах стандарта, а "по понятиям": например, в итоге должна получаться синхронная схема — этакая направленная ациклическая паутинка из комбинационной логики (сложение, умножение, деление, логические операции, ...), прилепленная своими входами и выходами к выходам и входам триггеров, соответственно. Триггер по фронту тактового сигнала запоминает на весь следующий такт значение своего входа, уровень которого должен быть стабилен сколько-то времени до фронта и сколько-то — после (это две временные константы). Сигналы с выходов триггеров, в свою очередь, после тактового сигнала начинают свой забег к выходам комбинационной логики (а значит, входам других триггеров. Ну, и выходам микросхемы), которая характеризуется также двумя интервалами: время, в течение которого ни один выход ещё не успеет начать изменяться, и время, через которое изменения успокоятся (при условии, что вход изменился единожды). Вот максимальная частота, при которой комбинационная логика обеспечивает выполнение требований триггеров — и есть Fmax. Когда схема между двумя тактами должна больше успеть посчитать, Fmax уменьшается. Конечно, хочется, чтобы частота была побольше, но если она вдруг подскочила в 10 раз (а то и количество частотных доменов в отчёте САПР уменьшилось) — проверьте, возможно, вы где-то что-то напутали, и в результате САПР нашёл константное выражение и радостно его использовал для оптимизации. Нет, не та, после которой идёт скрутка осциллографа и горстка лишних деталей, а oscilloscope bootstrapping — это как compiler bootstrapping, только для осциллографа. Мы будем делать осциллограф, по команде записывающий сколько-то отсчётов входного сигнала, после чего лишь отображающий записанное. Поскольку ему нужно будет как-то дать команду на запись, а после — навигироваться по ней, нам потребуются некие контроллеры кнопок — я написал не очень удобный, но совсем примитивный, вот он: Вот так будет выглядеть осциллограф: А так — контроллер, обрабатывающий нажатия клавиш: В коде осциллографа можно посмотреть на пример работы с регистровым файлом (возможно, не вполне корректный), а вот в контроллере есть кое-что интересное: в его конструктор вторым аргументом легко и непринуждённо мы передаём — нет, не контроллер кнопки — а лямбду, его создающую в нужном классу количестве (в данном случае — две штуки). Нужно было бы — мы бы этой лямбде и аргументы передали! Интересно, а Verilog так умеет?.. Вот так выглядит график изначально-цифрового сигнала, никогда не покидавший FPGA:  А так — выданный (только уже не с ШИМа на IO7, а с VGA_GREEN посредством R-2R ЦАП) и оцифрованный обратно с помощью микросхемы АЦП Марсохода:  В общем долго ли, коротко — и так пытался, и эдак, а цвет всё не появлялся. На Википедии даже есть шуточная расшифровка аббревиатуры PAL — "Picture At Last (Наконец-то, картинка!)" Код на GitHub. Scala + Chisel образуют современный язык описания цифровой аппаратуры — если для выразительности потребуется, то и с поддержкой функциональщины и всяких Higher-kinded types. А с помощью обычного Scala-плагина Идеи, ничего про Chisel не знающего, на нём ещё и очень приятно программировать. Причём всё это бесплатно и без привязки к САПР производителя конкретных микросхем ПЛИС. В общем — красота! Читатель возможно спросит: "А где же хэппи-энд?" — А НЕТ ЕГО! Но есть осциллограф... Программист