Подключение ddr к плис
Мирно гуглил на предмет того, какие рекомендуют способы подключения DDR памяти к FPGA (в народе «плисина»). Хотел улучшить цифровой логический анализатор сигналов, который щас собран на AVR и больше десятков милисекунд в SRAM на 12МГц-ах не может запомнить (существенно меньшие частоты пропихиваются прямо в USB) Думал, неплохо бы писать на планку памяти измеренное-места вдоволь, а потом потихоньку по USB на хост.
Удивился, найдя чудное решение от Gigabyte: I-RAM. Девайс, конечно, баян, на ЛОРе обсуждался здесь, я к своему стыду впервые слышу.
Прочитав о прародителе I-RAM, обзор и тесты первой версии I-RAM на ixbt и на THG, свежем форке I-RAM от ACARD - ANS-9010X , нескромно пришёл к мнению что есть повод поднять эту тему ещё раз.
Дело в том, что по универу знаком с ПЛИС, изучал и программировал.
На стартер-ките Xilinx'a Spartan-3, купленным моей кафедрой для тестирования программ на реальной железке, стоит XC3S200-FT256, условно 1/4 от того что стоит на I-RAM первой версии (XC3S1000-FT256).
Заглянул на сайт Xilinx и обнаружил, что у них на месте не стоит работа, и они щас выпускают серии Spartan-6 и Virtex-6, 5, и др., но самое кульное помимо всяких плюшек аля DSP или GPT 3.125Gbps, это то что у них почти всех «Support access rates of up to 800 Mbps using integrated memory controllers», а именно интегрированные два или четыре контроллера DDR, DDR2, DDR3. При желании не составляет труда найти на оффсайте application note под сабжевое семейство, где рассказано «как оно работает и какие ноги надо куда поцепить». С оффсайта Xilinx ссылка «Buy online» ведёт в магазин, где можно облизаться, увидев цены на представителей семейства от 50 баксов за 1 штуку, есть и 100 и 150, а экземпляры Virtex стоят под 1000.
На ПЛИС можно изобразить практически всё, что угодно. Ограничивающий фактор - системные вентили и логические ячейки. Чем их больше, тем большего объёма программа влезет в девайс, но тем дороже сама ПЛИС.
А теперь вопрос темы: стоит ли набрав долларов 200-300 за саму ПЛИС+обвязка+промышленное изготовление 4-слойной платы, вместо читания ЛОРа сидеть и разрабатывать такой Solid State Disk aka RAM-DRIVE девайс для SOHO на DDR3? Может быть только для себя, но и можно для других людей, которые хотят больше быстрой памяти на десктоповой системе.
В сервера ясно что можно ткнуть 100-200 Гиг не самой правда дешёвой оперативы и посмеятся надо мной. А если на серверную мать+обычно минимум два недешёвых проца моей стипухи за целый семестр, а то и за 5 курсов не хватит? Жду ваших мнений, где поставить запятую в топике?
В предыдущей части мы собрали микроконтроллер вообще без оперативной памяти на базе ПЛИС Altera/Intel. Однако на плате есть разъём с установленным SO-DIMM DDR2 1Gb, который, очевидно, хочется использовать. Для этого нам потребуется обернуть DDR2-контроллер с интерфейсом ALTMEMPHY в модуль, понятный для протокола работы с памятью TileLink, используемого повсюду в RocketChip. Под катом — тактильная отладка, брутфорс программирование и ГРАБЛИ.
Как известно, в Computer Science есть две главные проблемы: инвалидация кешей и именование переменных. На КДПВ вы видите редкий момент — две главные проблемы CS встретили друг друга и что-то замышляют .
DISCLAIMER: В дополнение к предупреждению из предыдущей статьи настоятельно рекомендую дочитать статью до конца перед повторением опытов, во избежание повреждения ПЛИС, модуля памяти или цепей питания.
Лирическое отступление: Существует, насколько я понимаю, довольно популярная серия «внутричиповых» интерфейсов AXI, разработанная ARM. Тут можно было бы подумать, что оно всё насквозь патентованое и закрытое. Но после того, как я зарегистрировался (безо всяких «университетских программ» и прочего — просто по e-mail и заполнению анкеты) и получил доступ к спецификации, меня ждало приятное удивление. Я, конечно, не юрист, но похоже, что стандарт довольно таки открытый: вы либо обязаны использовать лицензированные ядра от ARM, либо вообще не претендовать на совместимость с ARM, и тогда вроде всё ОК. Но вообще, конечно, читайте лицензию, читайте с юристами и т.д.
Задача казалась довольно простой, и я открыл описание уже имевшегося в проекте от поставщика платы модуля ddr2_64bit :
Народная мудрость гласит: «Любую документацию на русском языке нужно начинать со слов: "Итак, оно не работает"». Но здесь не совсем интуитивно понятный интерфейс, поэтому всё же почитаем. В описании нам тут же рассказывают, что работа с DDR2 — дело непростое. Нужно настроить PLL, провести некую калибровку, крекс-фекс-пекс, выставился сигнал local_init_done , можно работать. Вообще, логика именования здесь примерно следующая: имена с префиксами local_ — это «пользовательский» интерфейс, порты mem_ нужно непосредственно вывести на ножки, подключённые к модулю памяти, на pll_ref_clk нужно подать тактовый сигнал с указанной при настройке модуля частотой — из него будут получены остальные частоты, ну и всякие входы-выходы reset и выходы частот, синхронно с которыми должен работать пользовательский интерфейс.
Давайте создадим описание внешних сигналов к памяти и интерфейса модуля ddr2_64bit :
Тут меня поджидал первый букетик граблей: во первых, насмотревшись на класс ROMGenerator , я подумал, что и контроллер памяти можно выдернуть из глубин дизайна через глобальную переменную, а Chisel как-нибудь сам провода пробросит. Не получилось. Поэтому пришлось сделать жгут проводов MemIfBundle , который протягивался по всей иерархии. Почему же он не торчит из BlackBox -а, и не подключается разом? Дело в том, что у BlackBox все внешние порты запихнуты в val io = IO(new Bundle < . >) . Если в бандле весь MemIfBundle сделать одной переменной, то имя этой переменной будет сделано префиксом для имён всех портов, и имена банально не сойдутся с интерфейсом блока. Наверное, можно сделать как-то более адекватно, но пока оставим так.
Далее по аналогии с другими TileLink-устройствами (преимущественно живущими в rocket-chip/src/main/scala/tilelink ), и в особенности, BootROM , опишем свой интерфейс к контроллеру памяти:
По стандартному ключу ExtMem мы извлекаем из конфига SoC параметры внешней памяти (вот этот странный синтаксис позволяет по аналогии с паттерн-матчингом сказать «я знаю, что мне вернут экземпляр case class MemoryPortParameters (это гарантируется типом ключа на этапе компиляции Scala-кода, при условии, что в рантайме мы не упадём, вынимая содержимое из Option[MemoryPortParams] , равного None , но тогда нечего было контроллер памяти создавать в System.scala . ), так вот, сам case class мне не нужен, а некоторые его поля нужны»). Далее мы создаём manager port TileLink-устройства (протокол TileLink обеспечивает взаимодействие практически всего, что связано с памятью: контроллера DDR и других memory-mapped устройств, кешей процессора, возможно, ещё чего-то, у каждого устройства может быть по нескольку портов, каждое устройство может быть и manager, и client). beatBytes , насколько я понимаю, задаёт размер одной транзакции, а у нас обмен с контроллером ведётся по 16 байт. HasAltmemphyDDR2 и HasAltmemphyDDR2Imp мы подмешаем в нужных местах в System.scala , напишем конфиг
Видимо, пришло время почитать документацию на контроллер дальше списка портов. Так, что тут у нас. Упс, оказывается, входы-выходы с префиксом local_ должны выставляться синхронно не с pll_ref_clk , который 25MHz, а либо с phy_clk , выдающим половинную частоту памяти для half-rate контроллера, либо, в нашем случае, aux_half_rate_clk (может, всё-таки aux_full_rate_clk ?), выдающим полную частоту памяти, а она, на минуточку, 166MHz.
Стало быть, нужно пересекать границы частотных доменов. По старой памяти решил воспользоваться защёлками, точнее цепочкой из них:
Но, повозившись часок, пришёл к выводу, что не осилю на «скалярных» защёлках две очереди (в высокочастотный домен и обратно), каждая из которых будет иметь противонаправленные сигналы ( ready и valid ), да ещё и так, чтобы быть уверенным, что какой-нибудь битик не отстанет на такт-другой по дороге. Ещё через некоторое время я понял, что и описать синхронизацию на ready - valid без общего тактового сигнала — тоже задача сродни созданию неблокирующих структур данных в том смысле, что думать и формально доказывать нужно много, ошибиться легко, заметить трудно, а главное, всё уже реализовано до нас: у Intel есть примитив dcfifo , который представляет собой очередь конфигурируемой длины и ширины, читается и пишется которая из разных частотных доменов. В итоге я воспользовался экспериментальной возможностью свежего Chisel, а именно, параметризованными black box-ами:
И написал простенькую биндилку произвольных типов данных:
Чтобы локализовать проблему, решил банально закомментировать весь код внутри withClock(ddr_clock) (не правда ли, визуально похоже на создание потока) и заменить его заглушкой, которая точно работает:
Как я уже потом понял, эта заглушка тоже не работала по причине, что конструкция Wire(. ) , которую я добавил «для надёжности», чтобы показать, что это именно именованный провод, на самом деле использовала аргумент лишь как прототип для создания типа своего значения, но не привязывала его к выражению-аргументу. Также при попытке прочитать, что же всё-таки сгенерировалось, я понял, что в режиме симуляции имеется богатый выбор assertion-ов по поводу несоблюдения протокола TileLink. Они мне ещё наверняка пригодятся позже, но пока обошлось без попытки запустить симуляцию — а в чём её запускать? Verilator наверняка не знает про Alter-овские IP Cores, ModelSim Starter Edition скорее всего откажется симулировать такой огромный проект, но у меня он ещё и ругался на отсутствие модели контроллера для симуляции. А чтобы её сгенерировать, наверняка нужно сначала перейти на новую версию контроллера (потому что старый был настроен в древнем Quartus-е).
На самом деле, блоки кода были взяты из почти работающей версии, а не той, что активно отлаживалась за несколько часов до этого. Но вам же лучше ;) Кстати, постоянно пересобирать дизайн можно быстрее, если настройку WithNBigCores(1) заменить на WithNSmallCores(1) — с точки зрения базовой функциональности контроллера памяти разницы, вроде бы, нет. И ещё маленькая хитрость: чтобы не вбивать в gdb каждый раз одни и те же команды (там, по крайней мере у меня, нет сохранения истории команд между сессиями), можно просто сразу в командной строке набрать что-то вроде
и запускать по мере надобности штатными средствами командного интерпретатора.
В итоге был получен такой вот код работы с контроллером:
Тут мы всё-таки немного поменяем критерий завершённости: я уже видел, как безо всякой работы с памятью записанные данные как будто бы читаются, потому что кеш. Поэтому скомпилируем простенький кусочек кода:
В итоге получим следующий фрагмент ассемблерного листинга, инициализирующий первые 16 Мб памяти:
Его и вставим в начала bootrom/xip/leds.S . Теперь на одном лишь кеше вряд ли всё сможет держаться. Осталось запустить Makefile, пересобрать проект в Quartus, залить его в плату, подключиться OpenOCD+GDB и… Предположительно, ура, победа:
Так ли это, узнаем в следующей серии (я пока тоже не могу сказать про производительность, стабильность и т.д.).
На днях ко мне в руки попала EBV SoCrates Evaluation Board. В двух словах — это плата с SoC от фирмы Altera, на борту которой есть двухъядерный ARM и FPGA Cyclone V.
ARM и FPGA на одном чипе — это должно быть очень интересно! Но для начала всё это добро нужно «поднять».
Об этом процессе я и поведаю в данной статье.
Если вам в руки попала такая или подобная плата и вы не до конца уверены, что же с ней нужно делать. Если вы всегда думали, что FPGA — это что-то сложное и непонятно, как к этому подступиться. Или вы просто любопытный инженер. Тогда заходите. Мы всем рады.
А в качестве маленького бонуса измерим пропускную способность между CPU и FPGA.
План работ
- Получение прошивки FPGA
- Сборка ядра
- Сборка U-Boot и Preloader
- Сборка rootfs
- Написание тестовых программ
- Создание SD-карты
- Запуск платы и измерение пропускной способности
Сборка ядра
Теперь соберём ядро для нашего ARM.
Из инструментов потребуется Altera SoC EDS. Отсюда мы будет брать компилятор arm-linux-gnueabihf- для кросс-компиляции.
Запускаем скрипт, который добавит в PATH директории с компилятором и запустит bash:
Устанавливаем переменные окружения:
Переходим в директорию с ядром и выполняем конфигурацию:
Cобираем образ ядра для U-Boot:
Теперь нам нужно получить так называемый .dtb (Device Tree Blob) файл. Это бинарный файл, содержащий информацию о платформе — интерфейсы, пины, тактовые сигналы, адресное пространство и т.д. Ядро читает этот файл во время инициализации и вносит в неё изменения. Это позволяет использовать одно собранное ядро на нескольких аппаратных платформах.
Итак, получаем .dtb файл:
Но этот файл не для нашей платформы, поэтому нам придётся внести в него небольшие изменения. Для этого конвертируем файл в текстовый формат .dts (Device Tree Source):
Теперь в soc.dts нужно удалить блок bridge@0xff200000. Это можно сделать либо руками, либо наложив патч:
Теперь конвертируем файл обратно в .dtb:
- arch/arm/boot/uImage
- soc.dtb
План работ
- Получение прошивки FPGA
- Сборка ядра
- Сборка U-Boot и Preloader
- Сборка rootfs
- Написание тестовых программ
- Создание SD-карты
- Запуск платы и измерение пропускной способности
Полезные ссылки
В статье описано подключение к процессорной системе подсистемы оперативной памяти с использованием MIG 7 Series в Vivado 2018.3. Будут рассмотрены основные моменты настройки IP ядра и его подключение к процессору MicroBlaze.
Память DDR очень часто является неотъемлемой частью многих проектов и охватывает широкий круг задач, потому включение в проект контроллеров памяти просто необходимо. MIG 7 Series поддерживает всю седьмую серию FPGA Xilinx, а также SoC Zynq-7000, и позволяет подключать DDR3 и DDR2 SDRAM, QDR II + SRAM, RLDRAM II / RLDRAM 3 и LPDDR2 SDRAM. [1]. Однако при сборке процессорной подсистемы с Microblaze возможно настроить контроллер для работы только с DDR3 и DDR2 SDRAM. Вероятнее всего, это связано с наличием AXI4 интерфейса только для этих конфигураций памяти и отсутствием такового у других. Однако эта тема выходит за рамки данной статьи, но, возможно, будет затронута в будущем.
Целью статьи является описание основных настроек MIG 7 для памяти DDR3 и DDR2 SDRAM и пояснение этапов подключения подсистемы к ядру MicroBlaze.
Перед началом настройки необходимо создать проект в Vivado. У меня на текущий момент установлена версия Vivado 2018.3, и создавать проект я буду под Spartan с 50К ячеек, который стоит на плате ARTY S7 от Digilent [2]. Так как это уже было описано ранее [3], повторяться не будем и сразу перейдем к моменту добавления IP Core в блок дизайне.
После создания проекта и блок дизайна мы видим следующее окно (рисунок 1):
Рисунок 1 – Окно Block Design
Теперь мы можем добавлять различные ядра, включая MIG 7, с него и начнем. Откроем каталог доступных ядер и введем MIG в строке поиска. Мы должны увидеть следующее окно (рисунок 2):
Рисунок 2 – Окно с доступными ядрами
Дважды кликаем по нему и теперь можем увидеть, как он добавился в наш проект (рисунок 3).
Рисунок 3 – Окно Block Design с добавленным MIG 7
Дважды щелкнем мышкой по нему и вызовем окно настройки контроллера памяти (рисунок 4).
Рисунок 4 – Окно настройки MIG
Рисунок 5 – Вкладка настройки общих параметров
Тут мы имеем возможность выбора создания нового или верификации дизайна предыдущими настройками. Для плат Digilent’a можно найти эти настройки на гитхабе [5] и скачать или склонировать себе на компьютер. Также можно выбрать количество контроллеров в системе. Последняя неактивная вкладка должна давать нам возможность выбора интерфейса с шиной AXI. Но активной она становится только при запуске MIG из IP каталога в отдельной вкладке. Тут мы можем увидеть, что только контроллеры памяти DDR3 и DDR2 могут поддерживать эту опцию, и только если выбран Verilog как основной язык. На этой вкладке мы ничего не будем менять и потому продолжим на вкладке Pin Compatible FPGA (рисунок 6).
Рисунок 6 – Вкладка Pin Compatible FPGA
На текущей вкладке мы можем выбрать дополнительные FPGA, совместимые с нашей по корпусу и температуре. При выборе дополнительно совместимых FPGA, MIG будет резервировать только те выводы, которые являются общими для всех выбранных микросхем. Так как сейчас нам это не нужно, мы можем продолжить и перейти к вкладке Memory Selections (рисунок 7).
Рисунок 7 – Вкладка Memory Selection
Здесь мы выбираем необходимую нам память. На платах Digilent’a стоят микросхемы памяти DDR3, поэтому для моей платы ничего менять не нужно. Нажимаем «продолжить» и переходим к следующей вкладке (рисунок 8).
Рисунок 8 – Вкладка Controller Options
На этой вкладке мы выбираем частоту контроллера, частоту системного клока и настраиваем память. Мои настройки на этой странице совпадают с настройками Digilent’a, но на других платах они могут отличаться. Частота памяти настраивается в соответствии с вашей FPGAи ее Speed Grade’ом. Вторая строка настройки определяет отношение частоты работы памяти к частоте пользовательского клока. В нашем проекте этим клоком будет тактироваться MicroBlaze.
Следующий блок настраивает физические параметры используемой памяти. Временные параметры можно выбирать из некоторого количества предустановленных конфигураций, но даже если ее найти не получилось, можно создать свою конфигурацию на основе одной из имеющихся (рисунок 9).
Рисунок 9 – Настройка временных параметров
Все временные параметры можно найти в документации на вашу память. Напряжение питания и ширина шины данных также находятся в этой документации.
Number of Bank Machines оставим по умолчанию, а параметр Ordering сменим на Normal. Это должно повысить эффективность, так как дает контроллеру изменять приоритеты обращения к памяти автоматически, но принципиально никак не должно повлиять на будущий вид этого IP. Теперь можно перейти к следующей вкладке (рисунок 10).
Рисунок 10 – Вкладка AXI Parameters
На данной вкладке требуется настроить внутреннюю шину, через которую наше ядро будет подключаться к Microblaze. Ширину шины данных можно выбрать из предложенных значений, но не меньше ширины подключаемого пользовательского интерфейса. Вторая вкладка определяет порядок приоритетов чтения и записи. Третья вкладка позволяет включить поддержку Narrow Burst. Она необходима, если ширина шин устройств не совпадает, однако, как указано в документе [4], XPS в состоянии сама рассчитать необходимость ее поддержки после анализа подключенных мастеров к шине. Оставим все настройки по умолчанию и перейдем к вкладке Memory Options (рисунок 11).
Рисунок 11 – Вкладка Memory Options
На этой вкладке мы настраиваем входные и выходные клоки, а также импеданс буферов и терминальных резисторов. На ARTY S7 в банке с DDR стоит генератор на 100 МГц, поэтому выбираем его как входной клок. Также дополнительно генерируем 200 МГц, они понадобятся нам в будущем для опорного клока. Помимо 200 МГц, можно вывести до пяти других частот, если такие понадобятся в нашем проекте. Если нужные частоты получить не удается, можно попытаться вернуться назад и изменить частоту памяти.
Burst Type and Length настраивает тип пакетных передач. Для DDR3 и DDR2 длина может быть только 8. Output Driver Impedance Control и ODT выставим в соответствии с настройками Digilent’a. При настройке другой платы выходной импеданс нужно искать в документации. Остальные настройки оставим по умолчанию и перейдем на вкладку FPGA Options (рисунок 12).
Рисунок 12 – Вкладка FPGA Options
Выбираем входную частоту как Single-Ended и опорную частоту как No Buffer. Опорную частоту можно тактировать и от входной частоты, если она входит в диапазон от 199 МГц до 201 МГц. Она нужна для тактирования модулей IDELAYCTRL и элементов IDELAY в банке ввода-вывода, подключаемом к памяти. Установим галочку напротив Internal Vref. Это позволит сэкономить выводы, отведенные под опору, и использовать их в проекте как Single-Ended. Однако это возможно только на скоростях передачи ниже 800 Mbps. Также оставим включение XADC в ядро, потому что использовать его в проекте больше не планируем. Пропустим вкладку Extended FPGA Options, потому что там ничего менять не будем, и перейдем на вкладку IO Planning Options (рисунок 13).
Рисунок 13 – Вкладка IO Planning Options
От выбора на этой вкладке зависит то, как будут назначены сигналы для памяти. Если ядро генерируется для нового проекта, то можно зарезервировать выводы в соответствии с группами байт. Мы же воспользуемся вторым вариантом, так как наши пины уже известны заранее. На следующей вкладке Pin Selection увидим следующую картинку (рисунок 14):
Рисунок 14 – Вкладка Pin Selection
Воспользуемся констрейнами от Digilent’a, нажав кнопку Read XDC/UCFи выбрав соответствующий файл из репозитория. Для каждого сигнала должны обновиться номера выводов и их стандарты. Нажимаем на кнопку Validate и, если все нормально, переходим к следующей вкладке (рисунок 15).
Рисунок 15 – Вкладка System Signals Selection
Выбираем выводы для задействованных сигналов. Так как внешним у нас является только системный клок, то назначаем ему один из предложенных выводов, предварительно сверившись со схемой отладочной платы. Генератор подключен на вывод R2, его и выбираем. Больше нас на этой вкладке ничего не интересует.
На оставшихся вкладках нет никаких настроек, потому с ними можно ознакомиться самому, принять лицензионное соглашение и на последней нажать кнопку Generate.
В результате чего наше ядро должно преобразоваться и принять следующий вид (рисунок 16):
Рисунок 16 – Окно Block Design с обновленным MIG 7
Подключение процессора к памяти
Теперь мы можем добавить MicroBlaze в наш проект. Нажимаем кнопку Add IP и добавляем его в рабочее пространство. Запустим Run Block Automation и установим следующие параметры (рисунок 17):
Рисунок 17 – Настройки для Microblaze
После завершения автоматизации появятся локальная память, модуль дебага и модуль сброса (рисунок 18).
Рисунок 18 – Окно Block Design после автоматизации
Добавим подтяжку к «1» для входов сброса, подключим опорную частоту к 200 МГц и выведем наружу сигналы памяти DDR. Окно теперь должно выглядеть вот так (рисунок 19):
Рисунок 19 – Окно Block Design после автоматизации
Теперь добавим внешний порт к нашей входной частоте. Нажмем один раз на sys_clk_i в блоке MIG 7, чтобы выделить его, затем в свободном поле щелкнем правой кнопкой мыши и выберем пункт Create Port. Должно открыться следующее окно (рисунок 20):
Рисунок 20 – Создание порта
Оставляем все по умолчанию и нажимаем Ok. Если просто выбрать опцию Make External, Vivado создаст внешний порт с именем, отличным от того, которое было создано в файле констрейнов, и на этапе синтеза и имплементации появится критическое предупреждение, указывающее на отсутствие в дизайне порта с именем sys_clk_i.
Добавим модуль UARTLite и сконфигурируем его следующим образом (рисунок 21):
Рисунок 21 – Конфигурация UARTLite
Установленная частота соответствует частоте, сгенерированной MIG 7 для пользовательского интерфейса. UART нам понадобится позже для теста памяти
Теперь запустим Run Connection Automation для кэша MicroBlaze (рисунок 22).
Рисунок 22 – Run Connection Automation для кэша MicroBlaze
Теперь запустим автоматизацию подключения периферии (рисунок 23).
Рисунок 23 – Run Connection Automation для периферии
После этого нажмем кнопку Optimize Routing и получим следующее поле блок дизайна (рисунок 24):
Рисунок 24 – Окно Block Design после регенерации
Теперь можно запустить Validate Design, и, если все правильно, мы получим следующее уведомление (рисунок 25):
Рисунок 25 – Уведомление об успешной проверке
Запустим синтез, предварительно создав обертку нашего блок дизайна, и немного подождем.
После синтеза откроем I/O Planning и назначим оставшимся сигналам порты. Сохраним свой констрейн файл и запустим генерацию битстрима. Если никаких ошибок и критических предупреждений не появилось, то после ее окончания выберем File -> Export -> Export Hardware без битстрима, а затем запустим SDK.
Запуск проверки памяти
После запуска SDK создадим Application Project (рисунок 26):
Рисунок 26 – Создание нового проекта
Выставим следующую конфигурацию в первой вкладке создания проекта (рисунок 27):
Рисунок 27 – Конфигурация на первой вкладке
На второй вкладке выберем шаблон проекта Memory Tests (рисунок 28).
Рисунок 28 – Шаблоны проектов
Теперь добавим и запустим SDK Terminal со следующими настройками, исключая номер COM-порта, так как он у вас может быть любым другим (рисунок 29):
Рисунок 29 – Настройки COM-порта
Теперь настроим Run Configuration (рисунок 30).
Рисунок 30 – Настройка Run Configuration, вкладки Target Setup
И во второй вкладке (рисунок 31):
Рисунок 31 – Настройка Run Configuration, вкладки Aplication
Теперь запустим программирование нашей FPGA. Если все было сделано правильно, то в строке терминала мы увидим похожую запись (рисунок 32):
Это означает, что тест нашей памяти был пройден и все настроено правильно. Теперь можно самостоятельно посмотреть файлы тестового проекта и попытаться в них разобраться. Если возникли ошибки, то можно вернуться к самому началу или попытаться почитать предыдущие статьи о настройке процессорной системы MicroBlaze.
Цель данного туториала - повторить создание проекта Hello World с выводом текстовой строки в последовательный интерфейс UART, но на плате EBAZ4205. Для этого нужно будет учесть аппаратные особенности этой платы. Так же, отличием этого туториала является использование Vitis вместо более раннего варианта SDK.
Программатор
В качестве программатора использую китайский Platform Cable USB II Model DLC10
Обзор платы
Плата EBAZ4205 является управляющей картой от крипто майнера Ebit E9 + BTC. Сейчас как майнер стала не актуальна, поэтому б/у платы появились по доступной цене (порядка 1000р) на ebay и aliexpress (поисковая фраза "ZYNQ 7000"). При покупке я выбрал более дорогой вариант, где продавец припаивает слот для MicroSD карточки на плату. По умолчанию слот не припаян. Кроме слота, продавец припаял еще разъем для подключения UART и JTAG для программирования. Кроме этого, перепаян резистор R2577, R2584 для загрузки системы не с flash а с MicroSD.
Создание SD-карты
- soc.rbf
- uImage
- soc.dtb
- preloader-mkpimage.bin
- u-boot.img
- u-boot-env.img
- rootfs.tar.gz
- mem.o
- memblock.o
- /dev/sdb1 — для Preloader и U-Boot
- /dev/sdb2 — для файловой системы
На всякий случай затираем всё нулями.
Внимание! Eще раз проверьте, что /dev/sdb — это карта, а не Ваш второй жёсткий диск.
Для того, чтобы создать разделы, воспользуемся утилитой fdisk:
Далее нужно ввести следующие команды (пустая строка — ввод Enter):
Можно проверить, что у нас получилось:
Должно быть что-то похожее на:
Теперь скопируем на карту образ с переменными U-Boot:
После этого копируем Preloader:
И сам U-Boot:
Создаём файловую систему ext3:
И разворачиваем в неё нашу rootfs:
Далее копируем образ ядра, dtb, прошивку FPGA и тестовые программы:
Отмонтируем файловую систему:
Всё, карта готова!
Параметры платы
ПЛИС XC7Z010CLG400, Dual Core Cortex A9 @ 666.66MHz and Artix-7 FPGA with 28k LEs.
Внешняя память DDR3 256MB
Внешняя FLASH память - 128MB SLC NAND FLASH.
Ethernet трансивер для сети 10/100Mbps: IP101GA
Как становится ясно, плата требует небольшой доработки перед использованием. Для того, чтобы подать питание на плату, нужно запаять недостающий диод с обратной стороны.
После чего, питание можно будет подавать через стандартный разъем, похожий на Molex 6pin. Я же для подачи питания, использую раэъем подключения вентиляторов. Напряжение 5..12 вольт, ток 500 мА.
Сборка rootfs
Это раздел написан для тех, кто использует Debian (или в если Вашем дистрибутиве тоже есть debootstrap). Если Вы не среди них — можете воспользоваться Yocto или любым другим удобным для Вас методом.
Устанавливаем необходимые пакеты:
Создаем директорию и выкачивает туда необходимые файлы:
Чтобы запускать приложения, собранные под ARM-архитектуру, будем использовать qemu static. Для этого скопируем файл в нашу rootfs:
Переходим в нашу новую файловую систему:
В /etc/inittab оставляем следующие строки:
Запуск платы и измерение пропускной способности
Наконец-то всё готово для работы. Вставляем карту, подключаем USB и питание.
Заходим по консоли:
Первым делом прошьём FPGA.
Для это необходимо установить переключатель P18 на плате в положение «On On On On On» (выключатели с 1 по 5).
Смотрим текущее состояние FPGA:
Мы должны увидеть configuration phase
Заливаем прошивку:
И смотри состояние еще раз:
Состояние должно смениться на user mode. Это означает, что ПЛИС сконфигурирована и готова к работе.
Теперь проверяем наши утилиты. Но перед этим ещё немного «работы напильником».
У нашего кросс-компилятора и у Debian разные названия динамического линкера. Поэтому для того, чтобы утилиты работали, нам необходимо создать ссылку на правильный линкер:
Итак, запускаем утилиту (пояснение, что это за адрес, будет чуть ниже):
Если в результате Вы видите строку data = 0x00000007, значит всё в порядке.
Как я уже писал выше, внутренняя память ПЛИС у нас будет отображена в адресное пространство начиная с адреса 0xC0000000. Но перед тем, как мы сможем работать с этой памятью, нам нужно сделать еще два действия.
- 0-й — сброс интерфейса HPS-to-FPGA
- 1-й — сброс интерфейса LWHPS-to-FPGA
- 2-й — сброс интерфейса FPGA-to-HPS
После этого вновь прочитаем регистр, чтобы убедиться, что данные записались корректно:
Второе — мы должны включить отображение интерфейса HPS-to-FPGA в адресное пространство CPU. За это отвечает блок L3 (NIC-301) GPV (l3regs) с базовым адресом 0xFF800000, и конкретно его регистр remap со смещением 0. За HPS-to-FPGA отвечает бит под номером 3. В итоге, нам нужно записать в регистр число 0x8:
К сожалению, этот регистр доступен только для записи, поэтому прочитать для проверки данные у нас не получится.
Теперь мы можем читать и писать в память FPGA. Проверим это. Читаем:
Естественно, там должны быть нули. Теперь запишем туда что-нибудь:
И снова прочитаем:
Должно совпасть с записанным.
Ура! Мы наконец-то сделали это! Мы получили работающую SoC с FPGA и организовали доступ к её памяти из CPU.
Но просто читать/писать — это как-то совсем скучно. Давайте хотя бы измерим пропускную способность нашего интерфейса. Тем более это займет совсем немного времени.
Для этого нам потребуется наша вторая утилита memblock:
Она работает следующим образом: если первый аргумент cop равен 0, то в word_count 32-битных слов, начиная с адреса address, будет записана последовательность чисел от 0 до word_count-1. Вся процедура будет произведена cycles раз (это сделано для более точного измерения пропускной способности).
Если cop равен 1, то эти же слова будут считаны и выведены на экран.
Если cop равен 2, то слова будут считаны, а их значения будут сравниваться с теми, что гипотетически были записаны.
Проверим. Запишем немного данных:
Теперь считаем их:
Результат должен быть следующим:
Теперь попробуем сравнить данные, специально задав чуть большее количество слов:
Должны получить такую строку:
Теперь запускаем запись по всему объему памяти в количестве 1000-ти повторений и замеряем время записи:
Среднее значение по 5 запускам равно 11.17 секунд. Считаем пропускную способность:
Не очень густо. А что у нас с чтением:
Среднее время 10.5 секунд. Что выливается в:
Примерно то же самое. Естественно, на время выполнения любой из этих операций одно из двух ядер загружается на 100%.
Если при компиляции добавить флаг -O3, то пропускная способность на запись и на чтение станет 212 Мбит/c и 228 Мбит/c соответственно. Чуть лучше, но тоже не метеор.
Но это и не удивительно — мы же ничего не делали, чтобы эту самую пропускную способность увеличить. Неплохо было бы поиграться с более хитрой оптимизацией, посмотреть в сторону ядра, или, на худой конец, хотя бы прикрутить DMA, чтобы разгрузить процессор.
Но это уже в следующей статье, если, конечно, кому-то это будет интересно.
Спасибо тем, кто добрался до конца! Удачи!
Написание тестовых программ
Если говорить в двух словах, то почти всё взаимодействие между компонентами SoC происходит при помощи отображения адресного пространства одного компонента в адресное пространство другого.
Рассмотрим на примере. В нашем проекте при помощи Qsys мы указали, что на интерфейсе HPS-to-FPGA начиная с адреса 0 расположен блок On-Chip памяти размером 262144 байт. Сам интерфейс HPS-to-FPGA отображается в адресное пространство CPU по адресу 0xC0000000 (см. документацию на Cyclone V). В итоге обращение CPU по адресам от (0xC0000000 + 0) до (0xC0000000 + 262143) будет приводить к обращению ко внутренней памяти FPGA.
Поэтому для работы нам потребуется утилита, с помощью которой можно читать/писать про произвольным адресам памяти. Вот её исходный код:
Теперь нужно собрать её с использованием кросс-компилятора. Для этого запускаем скрипт:
И выполняем компиляцию:
Также нам нужна утилита для измерения пропускной способности:
Приступаем к работе
Еще одно отличие этой статьи от серии статей про Minized это использование Vivado 2020.2 и Xilinx Vitis 2020.2 (в отличие от SDK в более ранних версиях)
Сборка и настройка HW части
Создаем новый проект и попробуем повторить функционал, который выведет строчку "Hello world" чере UART порт (для того, чтобы это увидеть, нам потребуется переходник USB UART). В таком проекте потребуется настроить по-минимуму аппаратную часть ZYNQ: порт UART, DDR память; и написать прошивку для ядра.
Задаем имя проекта
Выберем модель ПЛИС
Дождемся инициализации проекта
В IP INTEGRATOR создаем блок Create Block Design имя оставлю по умолчанию
Добавлю систему ZYNQ7 на диаграмму
ZYNQ на диаграмме
Дважды кликаем по блоку ZYNQ на диаграмме, чтобы перейти в настройки системы ZYNQ
Переходим во вкладку PS-PL Configuration и на всякий случай снимаем галочку
Настроим аппаратные блоки, хотя некоторые могут и не потребоваться.
На вкладке MIO Configuration настроим SD и UART1
Если в будущих проектах планируется тактировать PL логику от системы ZYNQ в качестве источника, то это можно настроить во вкладке Clock Cinfiguration выход FCLK_CLK0
Далее, настроим параметры DDR
Там же параметры задержки
На этом все. Нажимаем "OK".
На диаграмме запускаем "Run Block Automation" оставляя параметры по умолчанию и тоже нажимаем "ОК"
На диаграмме увидим следующее
Переходим на вкладку Sources, выбираем диаграмму, создаем обертку и генерируем че-то там еще. Моя не понимать
Create HDL Wrapper - cоглашаемся, пусть система обновляет файлы в случае изменения дизайна.
В Generate тоже оставляем все без изменений
Далее запускаем Run Synthesis, Run Implementation, Generate Bitstream. В итоге можно открыть Implementation Design и посмотреть на него (как на новые ворота)
Далее экспортируем полученный результат
Пути и имя не меняем
Мы настроили аппаратную часть, экспортировали ее и готовы к написанию прошивки.
Сборка U-Boot и Preloader
- Boot ROM
- Preloader
- Bootloader
- OS
Функциями Preloader чаще всего являются инициализация SDRAM интерфейса и конфигурация пинов HPS. Инициализация SDRAM позволяет выполнить загрузку следующей стадии из внешней памяти, так как её код может не поместиться в 60 КБ доступной встроенной памяти.
Bootloader может участвовать в дальнейшей инициализации HPS. Также эта стадия выполняет загрузку операционной системы либо пользовательского приложения. Обычно (и в нашем случае) в качестве Bootloader выступает U-Boot.
OS — тут всё просто. Это наш любимый Linux. Ядро для него у нас уже есть, корневую файловую систему получим чуть позже.
А в сейчас мы займемся Preloader и U-Boot
Открываем терминал, запускаем уже знакомый нам скрипт:
Заходим в директорию с нашим проектом:
После компиляции там должна появиться директория hps_isw_handoff, переходим в неё:
Запускаем генерацию необходимых файлов:
После этого дожна появиться директория build.
Собираем Preloader:
Теперь нам нужно настроить переменные для U-Boot. Вначале создаем текстовый файл u-boot-env.txt.
Затем конвертируем его в бинарный формат, не забыв указать размер области, содержащей переменные — 4096 байт нам вполне хватит. Даже если реальный размер превысит заданный, mkenvimage сообщит об этом.
- build/uboot-socfpga/u-boot.img
- u-boot-env.img
- build/preloader-mkpimage.bin
Создание прошивки FPGA
Первым делом нам нужно получить прошивку FPGA.
Из инструментов для этого понадобится САПР Quartus, скачать его можно на официальном сайте
Описывать установку не буду — там всё достаточно очевидно.
Создание проекта
Запускаем Quartus, идём в File -> New Project Wizard, жмём Next, заполняем директорию и название проекта:
Остальные настройки для нас не важны, жмём Finish.
Проект Qsys
Qsys — отличный инструмент для начинающих. Позволяет получить прошивку, не написав ни строчки кода. Вместо этого разработчик собирает конструктор из заранее заданных кубиков (IP-корок). Требуется только правильно настроить каждую корку и соединить их должным образом.
- Processors and Peripherals -> Hard Processor Systems -> Arria V / Cyclone V Hard Processor System
- Basic Functions -> On Chip Memory -> On Chip Memory (RAM or ROM)
На первой вкладке нас интересует HPS-to-FPGA interface width, чтобы мы имели доступ из CPU ко внутренней памяти FPGA:
Дальше идёт куча настроек для различных интерфейсов — в каких режимах работают, какие пины используются:
С HPS мы разобрались, переходим к настройке On-Chip памяти. Это память, которая расположена непосредственно внутри ПЛИС.
Настроек тут значительно меньше:
Теперь нужно соединить блоки между собой. Всё достаточно интуитивно (обратите внимание на значение базового адреса напротив s1):
Готово. Сохраняем (File -> Save) под именем soc.
Осталось сгенерировать файлы. Кнопка Generate HDL, в появившемся окне опять жмём Generate, ждём, Finish.
Компиляция проекта
Теперь нужно добавить сгенерённые файлы в проект:
Assignments -> Settings вкладка Files, добавляем файл soc/synthesis/soc.qip
Нужно применить настройки для DDR пинов. Но перед этим нужно выполнить первую стадию компиляции:
Processing -> Start -> Start Analysis & Synthesis
Запускаем скрипт для настройки пинов:
Tools -> Tcl Scripts. В появившемся окне выбираем Project -> soc -> synthesis -> submodules -> hps_sdram_p0_pin_assignments.tcl, Run.
Финальная компиляция проекта:
Processing -> Start Compilation
Мы получили файл soc.sof c прошивкой FPGA. Но мы хотим прошивать ПЛИС прямо из CPU, поэтому нам понадобится другой формат. Выполним конвертацию. Это можно делать и из GUI, но в консоле проще. Да и вообще, пора уже отвыкать от GUI :).
Для конвертации надо запустить терминал и перейти в директорию с нашим проектом. Далее перейти в output_files и выполнить команду (не забываем, что директория с утилитами Quartus дожна быть в переменной PATH):
Ура! Мы получили прошивку FPGA.
Читайте также: