В прошлой статье цикла мы потренировались сохранять данные из потокового интерфейса в память средствами DMA. Пришла пора сделать какую-то полезную поделку, используя полученные навыки. Очень полезная при удалённой отладке вещь — анализатор. Вообще, при работе с комплексом скорее нужны специализированные шинные анализаторы, но начинать лучше с чего-то попроще. Поэтому сейчас мы сделаем простейший логический анализатор на 32 канала. Понятно, что он будет совсем-совсем примитивным, но зато мы сделаем его своими руками. У кого ещё нет комплекса Redd, могут повторить опыт, используя любую макетную плату с ПЛИС фирмы Altera (Intel) и микросхемой ОЗУ. Итак, приступаем.
На самом деле, самый-самый простейший анализатор мы сделали ещё в прошлый раз. Напомню, как выглядит шина AVALON_ST, скопировав рисунок из старой статьи:
То есть пробросили внешние линии на шину data, взвели сигнал valid, и началось запоминание по принципу «отсюда и до обеда». Ну, то есть, пока память не закончится. Так работал мой осциллограф смешанных сигналов RIGOL, так работал логический анализатор HANTEK. Если для осциллографа смешанных сигналов по-другому нельзя, ведь аналоговый сигнал всё время изменяется, а он сохраняется вместе с цифрой, то для логического анализатора такой подход – более, чем странен. Зачем сохранять данные без сжатия? В далёком 2007-м году добыл я китайский анализатор LA5034. Он был настолько китайским, что даже программа к нему сначала не имела английского интерфейса! Так вот, даже он уже не расходовал память на сохранение одних и тех же данных. Имея всего несколько килобайт ОЗУ (встроенного в ПЛИС), он позволял делать намного больше, чем дурацкий HANTEK с многомегабайтными микросхемами памяти.
В общем, нам сейчас предстоит для этой основы сделать систему сжатия данных. А вот кольцевой буфер, блок триггера и блок фильтрации потока мы сегодня делать не будем – всё-таки статья должна содержать что-то простенькое. Тем более, что я не описываю какую-то готовую разработку, я проектирую анализатор чисто ради статьи. Потом он, конечно, пойдёт в набор примеров для комплекса, но всё равно, много времени на разработку мне никто не даст. Так что сжатие – это святое, а триггеры и фильтрация потока – это каждый добавит сам, если оно ему понадобится.
Я выбрал самую простейшую методику сжатия. Линия задержки и компаратор прямого и задержанного на один такт сигнала.
Первый регистр на схеме выполняет чрезвычайно важную функцию. Нельзя работать с недискретизированными данными! Много лет назад я на этом обжёгся. У меня в проекте автомат переходил из одного состояния в другое. Всё бы ничего, но на графе переходов не было такой стрелки. В чём же дело? А я анализировал как раз «сырые», а не дискретизированные данные. В результате, они могли изменить своё состояние в любой момент. Как известно, внутри ПЛИС у линий GCK скорость распространения более-менее единая, у остальных же линий – совершенно произвольная. А состояние автомата задавалось в двоичном виде. То есть, для его хранения использовалось несколько битов, хранящихся в нескольких триггерах. В отличие от процессора, новое содержимое, которое защёлкнется в каждый бит, вычисляется независимо. И время прохождения сигнала в процессе этих вычислений от входа до триггера – тоже для каждого бита своё.
И вот. Надо нам, скажем, перейти из состояния 0000 в зависимости от условий, или в 0001 или в 0110. И вот условие перехода изменилось очень близко к тактовому импульсу. Давайте я обозначу красными те биты, до которых данные успеют добежать, поэтому они примут новые значения для перехода в 0001, а синими – те, до которых не успеют, и они примут значение для перехода в 0110. Итак: 0000.
В итоге, получаем состояние 0011. А на графе такого перехода не было! Кодирование методом OneHot не решит проблему, просто она станет очевидной (а так – пока я отловил врага, пока понял, кто виноват – 4 дня убил, ведь проявлялась беда очень редко, да и сначала я грешил на неверную реализацию логики).
Чтобы избежать этого, дискретизируем всё и вся! Что бы там ни защёлкнулось на входе, на выходе оно будет иметь стабильное состояние на протяжении такта. Поэтому на вход компаратора попадут уже стабильные данные!
Ну, а второй регистр будет иметь на выходе данные, которые пришли на прошлом такте… Если прошлое и текущее значения не совпадают — надо новое сохранить, для чего взводим сигнал valid.
У такой системы сжатия только один недостаток, но он делает её в таком виде совершенно неприемлемой. Мы не знаем, как долго держалось каждое стабильное состояние. Чтобы устранить данную проблему, добавим таймер. Если сейчас данные 32 бита, то имеет смысл добавить ещё 32-битный таймер, так как суммарная шина должна удваиваться, а после 32 идёт разрядность 64. Просто будем защёлкивать натикавшие показания. Зная значение таймера для прошлой и текущей записи, мы всегда поймём, как долго держалось прошлое значение. Правда, таймер имеет свойство переполняться. На частоте 100 МГц он переполнится через 42.9 секунды. Но ничто не мешает нам при нулевом значении таймера также произвести защёлкивание данных. Накладные расходы памяти будут не так велики, а программа догадается, что произошло переполнение и надо начать отмерять значения с начала. В итоге, получаем такую блок-схему:
64-битная шина данных при 16-битной микросхеме SDRAM – это не совсем хорошо. Допустим, мы тактируем ОЗУшку частотой 100 Мгц. Тогда, чисто теоретически, мы не можем использовать частоту дискретизации выше 25 МГц, ведь фактически каждое 64-битное слово будет уходить в ОЗУ в виде четырёх 16-битных слов. А практически, с поправкой на подачу команд микросхеме ОЗУ и циклы регенерации, предельная рабочая частота будет и того меньше. Может, даже 20 МГц.
Что на это можно сказать? Да, при разработке комплекса Redd не стояло задачи сделать супер производительный анализатор. Давайте взглянем на фирменные анализаторы, имеющиеся у меня под рукой. Вот простенький 16-битный. В нём стоит целых две ОЗУшины. Все ножки ПЛИС обслуживают каналы и ОЗУ. Ну, ещё на стык с USB уходят. А в Redd они ещё и для других целей используются.
Вот тот самый многострадальный абсолютно бесполезный HANTEK. Сжатия нет, но тоже две ОЗУшины. Причём, насколько я помню – DDR. В своё время, неплохо его изучил: несколько лет назад хотел сделать «прошивку» со сжатием, даже выпросил у производителя UCF-файл, но так и не освоил работу с ОЗУ у Xilinx. Но с тех пор я мог подзабыть детали схемы.
А вот так изнутри выглядит туловище анализатора LeCroy через отверстие под установку головы:
Там целых четыре модуля памяти с кучей микросхем каждый. Мне не хочется его сейчас вскрывать, но когда я отчищал его от пыли, сильно проникся внешним видом той ПЛИС, которая стоит внутри. Столько модулей памяти в параллель обслуживать – много ножек и ресурсов ПЛИСине требуется. И цена такого анализатора (разумеется, нового, а не с eBay) – десятки тысяч долларов. Насколько я помню, даже больше полусотни тысяч.
В целом, если кому-то позарез нужна производительность, он может или приобрести макетную плату с 32-битной ОЗУ, или разработать свою, установив туда две 32-битные ОЗУшины. Или даже модули DIMM. Но это уже будет BGA ПЛИС, у неё будет уже другая цена, и всё (включая класс печатной платы) другое. А теория – будет та же, что и сейчас, просто надо будет выкинуть преобразователь разрядности шины. Так что продолжаем рассуждения.
Вообще, на самом деле, и у нас всё не так плохо. Если данные идут небольшими пачками, то необходимо и достаточно установить блок FIFO. Пришла пачка – она попала в очередь. Дальше – на входе тишина, а данные из очереди постепенно уходят в ОЗУ. Таким образом, мгновенная производительность анализатора будет 100 МГц… Но в целом – всё будет хорошо при условии, что не переполняется FIFO. Именно поэтому я сделал целую статью, которая помогает оставить как можно больше памяти для нужд этого самого блока FIFO. Самое главное – блок должен быть установлен там, где шина данных ещё 64-битная. Итого, получаем блок-схему анализатора:
Ну что ж, приступаем к разработке головы. Я как-то привык к терминологии мощных шинных анализаторов, у которых имеется универсальное туловище, а уже к нему подключаются проблемно ориентированные головы. Поэтому и у нас будет туловище и голова. Для реализации выбранной схемы не нужно даже делать никаких автоматов.
Интерфейс модуля будет таким:
Вот так мы реализуем процесс, который защёлкивает данные в регистрах и увеличивает счётчик:
Первое условие записи:
Второе условие записи:
Результирующее условие записи:
Ну, и из опытов ясно, что байты на шине надо немного перекрутить:
Собственно, всё. Давайте для полноты картины я вставлю полный текст модуля в слитном варианте.
Как-то зловеще звучит заголовок… Но как бы там ни было, а упаковать всё в компонент нам надо. Мы тренировались делать подобное в этой статье.
У меня получилась шина AVALON_ST, штатные линии тактирования и сброса, и… Но сначала рисунок с типовыми вещами:
Из нетиповых: для будущей задумки линии conduit пришлось дать осознанное имя типу сигнала. Оно нам ещё пригодится.
В остальном – вроде, всё понятно.
Как мы уже рассматривали в этой статье, мы не станем добавлять в систему процессорное ядро Nios II, а воспользуемся блоком Altera JTAG-to-Avalon-MM.
Работать с контроллером SDRAM мы учились в этой статье, а в этой разбирались, как при помощи блока PLL разогнать систему до 100 Мгц. Экспериментировали с FIFO и изменением ширины шины AVALON_ST при помощи блока AVALON_ST_ADAPTER мы в этой статье. Наконец, с DMA мы экспериментировали буквально в прошлой статье.
Пришла пора собрать все эти знания в едином проекте! Вот такая у меня получилась навёрнутая структурная схема.
Страшно? Ничуть. Давайте пройдёмся по ней сверху вниз. Сначала идёт блок тактирования и сброса. Как всегда, для комплекса Redd, чтобы не мучиться, физическую ножку Reset я не использую (я её всегда виртуальной делаю). Так удобнее для данной конкретной аппаратуры, хоть и не совсем правильно. Тактирование же идёт на блок PLL. Как его настраивать, мы уже подробно рассматривали раньше. Если я вставлю сюда массу скриншотов, то сильно перегружу статью. С выхода c0 мы берём тактовый сигнал для всей нашей системы, а выход c1 – экспортируем и подключаем к тактовому входу микросхемы SDRAM.
Master0 – это тот самый компонент Altera JTAG-to-Avalon-MM, через который мы будем достукиваться до шины AVALON_MM. Он в настройках не нуждается. Доступ к шине нам нужен, чтобы управлять блоком DMA и чтобы считывать содержимое SDRAM с накопленными результатами.
Дальше идёт наш компонент «Голова». А уже из неё растекается поток через цепочку шин AVALON_ST. Сначала он затекает в блок FIFO. Это – первый блок, настройки которого стоит показать особо:
8 символов на слово, каждый символ 8 бит. Итого 8*8=64 бита. Ёмкость – 4 килослова. Все остальные вещи протокола AVALON_ST отключены. Двойное тактирование сделано для того, чтобы в будущем голова могла работать на частоте, отличной от частоты работы туловища. Это нам пригодится, когда мы будем делать шинный анализатор USB.
Дальше данные перетекают в преобразователь разрядности. Вот его настройки:
Собственно, 8 символов на слово на входе, 4 символа на слово – на выходе. 8 бит на символ. Тоже всё просто. Наконец, поток входит в блок DMA. Ему я только типы шин и максимальную длину передачи поправил, да выставил режим доступа только в режиме полного слова, чтобы поднять Fmax. По уму, такой огромный объём памяти дескрипторов не нужен (хотя, нутром чую, что через них мы можем реализовать кольцевой буфер для анализатора). Размер входного FIFO тоже можно уменьшить до минимума, ведь у нас есть FIFO до этого блока. Но, честно говоря, работа над статьёй и так уже затянулась, так что оставим эту оптимизацию для читателей в качестве самостоятельной работы.
Всё. Потоковая часть завершена. Дальше данные попадают в контроллер SDRAM. Напомню его настройки
Из функциональной части – всё. Но кто следит за рассказом не по диагонали, а внимательно, наверное, заметил ещё один странный блок DataGen_0. Что это такое? Мы раньше такого не применяли!
Дело в том, что мне же как-то надо проверить работу головы. А это надо все 32 линии назначить на какие-то ножки ПЛИС, подключить к ним какой-то источник… А все должны будут поверить мне на слово, что я это сделал. И потом думать, как это повторить у себя. Зачем? Давайте добавим тестовый генератор данных и подключим его не проводами, а через трассировочные ресурсы ПЛИС. Я сделал самый простой счётчик, который увеличивает своё значение в случайные моменты времени. В качестве генератора случайных чисел я взял 32-разрядную M-последовательность, а увеличиваю счётчик, когда в младших восьми битах появляется константа 0x12. Вот такой получился SystemVerilog код, реализующий эту функциональность (обратите внимание, что я по-прежнему не использую сигнал reset, хотя, здесь бы он пригодился):
В настройках компонента самая важная деталь – это имя параметра Signal Type у conduit шины. Он должен быть таким же, какой я заполнил у соответствующего параметра головы. В остальном – всё просто, здесь же нет никаких специальных шин, только conduit…
Соединяем соответствующие линии (это единственное соединение на структурной схеме выше, которое я не стал подсвечивать каким-либо цветом), получаем то, что нужно.
Делаем линию reset виртуальной. Всем остальным ножкам я предпочёл сделать назначение не в GUI, а скопировал фрагмент файла *.qsf из проекта, сделанного в самой первой статье.
Можно приступать к экспериментам… Но все уже устали, так что практикой мы займёмся в следующий раз.
Предыдущие статьи цикла
- Разработка простейшей «прошивки» для ПЛИС, установленной в Redd, и отладка на примере теста памяти.
- Разработка простейшей «прошивки» для ПЛИС, установленной в Redd. Часть 2. Программный код.
- Разработка собственного ядра для встраивания в процессорную систему на базе ПЛИС.
- Разработка программ для центрального процессора Redd на примере доступа к ПЛИС.
- Первые опыты использования потокового протокола на примере связи ЦП и процессора в ПЛИС комплекса Redd.
- Веселая Квартусель, или как процессор докатился до такой жизни.
- Методы оптимизации кода для Redd. Часть 1: влияние кэша.
- Методы оптимизации кода для Redd. Часть 2: некэшируемая память и параллельная работа шин.
- Экстенсивная оптимизация кода: замена генератора тактовой частоты для повышения быстродействия системы.
- Доступ к шинам комплекса Redd, реализованным на контроллерах FTDI
- Работа с нестандартными шинами комплекса Redd
- Практика в работе с нестандартными шинами комплекса Redd
- Проброс USB-портов из Windows 10 для удалённой работы
- Использование процессорной системы Nios II без процессорного ядра Nios II
- Практическая работа с ПЛИС в комплекте Redd. Осваиваем DMA для шины Avalon-ST и коммутацию между шинами Avalon-MM
Определяем функциональность анализатора
На самом деле, самый-самый простейший анализатор мы сделали ещё в прошлый раз. Напомню, как выглядит шина AVALON_ST, скопировав рисунок из старой статьи:
То есть пробросили внешние линии на шину data, взвели сигнал valid, и началось запоминание по принципу «отсюда и до обеда». Ну, то есть, пока память не закончится. Так работал мой осциллограф смешанных сигналов RIGOL, так работал логический анализатор HANTEK. Если для осциллографа смешанных сигналов по-другому нельзя, ведь аналоговый сигнал всё время изменяется, а он сохраняется вместе с цифрой, то для логического анализатора такой подход – более, чем странен. Зачем сохранять данные без сжатия? В далёком 2007-м году добыл я китайский анализатор LA5034. Он был настолько китайским, что даже программа к нему сначала не имела английского интерфейса! Так вот, даже он уже не расходовал память на сохранение одних и тех же данных. Имея всего несколько килобайт ОЗУ (встроенного в ПЛИС), он позволял делать намного больше, чем дурацкий HANTEK с многомегабайтными микросхемами памяти.
В общем, нам сейчас предстоит для этой основы сделать систему сжатия данных. А вот кольцевой буфер, блок триггера и блок фильтрации потока мы сегодня делать не будем – всё-таки статья должна содержать что-то простенькое. Тем более, что я не описываю какую-то готовую разработку, я проектирую анализатор чисто ради статьи. Потом он, конечно, пойдёт в набор примеров для комплекса, но всё равно, много времени на разработку мне никто не даст. Так что сжатие – это святое, а триггеры и фильтрация потока – это каждый добавит сам, если оно ему понадобится.
Методика сжатия потока
Я выбрал самую простейшую методику сжатия. Линия задержки и компаратор прямого и задержанного на один такт сигнала.
Первый регистр на схеме выполняет чрезвычайно важную функцию. Нельзя работать с недискретизированными данными! Много лет назад я на этом обжёгся. У меня в проекте автомат переходил из одного состояния в другое. Всё бы ничего, но на графе переходов не было такой стрелки. В чём же дело? А я анализировал как раз «сырые», а не дискретизированные данные. В результате, они могли изменить своё состояние в любой момент. Как известно, внутри ПЛИС у линий GCK скорость распространения более-менее единая, у остальных же линий – совершенно произвольная. А состояние автомата задавалось в двоичном виде. То есть, для его хранения использовалось несколько битов, хранящихся в нескольких триггерах. В отличие от процессора, новое содержимое, которое защёлкнется в каждый бит, вычисляется независимо. И время прохождения сигнала в процессе этих вычислений от входа до триггера – тоже для каждого бита своё.
И вот. Надо нам, скажем, перейти из состояния 0000 в зависимости от условий, или в 0001 или в 0110. И вот условие перехода изменилось очень близко к тактовому импульсу. Давайте я обозначу красными те биты, до которых данные успеют добежать, поэтому они примут новые значения для перехода в 0001, а синими – те, до которых не успеют, и они примут значение для перехода в 0110. Итак: 0000.
В итоге, получаем состояние 0011. А на графе такого перехода не было! Кодирование методом OneHot не решит проблему, просто она станет очевидной (а так – пока я отловил врага, пока понял, кто виноват – 4 дня убил, ведь проявлялась беда очень редко, да и сначала я грешил на неверную реализацию логики).
Чтобы избежать этого, дискретизируем всё и вся! Что бы там ни защёлкнулось на входе, на выходе оно будет иметь стабильное состояние на протяжении такта. Поэтому на вход компаратора попадут уже стабильные данные!
Ну, а второй регистр будет иметь на выходе данные, которые пришли на прошлом такте… Если прошлое и текущее значения не совпадают — надо новое сохранить, для чего взводим сигнал valid.
У такой системы сжатия только один недостаток, но он делает её в таком виде совершенно неприемлемой. Мы не знаем, как долго держалось каждое стабильное состояние. Чтобы устранить данную проблему, добавим таймер. Если сейчас данные 32 бита, то имеет смысл добавить ещё 32-битный таймер, так как суммарная шина должна удваиваться, а после 32 идёт разрядность 64. Просто будем защёлкивать натикавшие показания. Зная значение таймера для прошлой и текущей записи, мы всегда поймём, как долго держалось прошлое значение. Правда, таймер имеет свойство переполняться. На частоте 100 МГц он переполнится через 42.9 секунды. Но ничто не мешает нам при нулевом значении таймера также произвести защёлкивание данных. Накладные расходы памяти будут не так велики, а программа догадается, что произошло переполнение и надо начать отмерять значения с начала. В итоге, получаем такую блок-схему:
Производительность анализатора
64-битная шина данных при 16-битной микросхеме SDRAM – это не совсем хорошо. Допустим, мы тактируем ОЗУшку частотой 100 Мгц. Тогда, чисто теоретически, мы не можем использовать частоту дискретизации выше 25 МГц, ведь фактически каждое 64-битное слово будет уходить в ОЗУ в виде четырёх 16-битных слов. А практически, с поправкой на подачу команд микросхеме ОЗУ и циклы регенерации, предельная рабочая частота будет и того меньше. Может, даже 20 МГц.
Что на это можно сказать? Да, при разработке комплекса Redd не стояло задачи сделать супер производительный анализатор. Давайте взглянем на фирменные анализаторы, имеющиеся у меня под рукой. Вот простенький 16-битный. В нём стоит целых две ОЗУшины. Все ножки ПЛИС обслуживают каналы и ОЗУ. Ну, ещё на стык с USB уходят. А в Redd они ещё и для других целей используются.
Вот тот самый многострадальный абсолютно бесполезный HANTEK. Сжатия нет, но тоже две ОЗУшины. Причём, насколько я помню – DDR. В своё время, неплохо его изучил: несколько лет назад хотел сделать «прошивку» со сжатием, даже выпросил у производителя UCF-файл, но так и не освоил работу с ОЗУ у Xilinx. Но с тех пор я мог подзабыть детали схемы.
А вот так изнутри выглядит туловище анализатора LeCroy через отверстие под установку головы:
Там целых четыре модуля памяти с кучей микросхем каждый. Мне не хочется его сейчас вскрывать, но когда я отчищал его от пыли, сильно проникся внешним видом той ПЛИС, которая стоит внутри. Столько модулей памяти в параллель обслуживать – много ножек и ресурсов ПЛИСине требуется. И цена такого анализатора (разумеется, нового, а не с eBay) – десятки тысяч долларов. Насколько я помню, даже больше полусотни тысяч.
В целом, если кому-то позарез нужна производительность, он может или приобрести макетную плату с 32-битной ОЗУ, или разработать свою, установив туда две 32-битные ОЗУшины. Или даже модули DIMM. Но это уже будет BGA ПЛИС, у неё будет уже другая цена, и всё (включая класс печатной платы) другое. А теория – будет та же, что и сейчас, просто надо будет выкинуть преобразователь разрядности шины. Так что продолжаем рассуждения.
Вообще, на самом деле, и у нас всё не так плохо. Если данные идут небольшими пачками, то необходимо и достаточно установить блок FIFO. Пришла пачка – она попала в очередь. Дальше – на входе тишина, а данные из очереди постепенно уходят в ОЗУ. Таким образом, мгновенная производительность анализатора будет 100 МГц… Но в целом – всё будет хорошо при условии, что не переполняется FIFO. Именно поэтому я сделал целую статью, которая помогает оставить как можно больше памяти для нужд этого самого блока FIFO. Самое главное – блок должен быть установлен там, где шина данных ещё 64-битная. Итого, получаем блок-схему анализатора:
Разработка головы анализатора
Ну что ж, приступаем к разработке головы. Я как-то привык к терминологии мощных шинных анализаторов, у которых имеется универсальное туловище, а уже к нему подключаются проблемно ориентированные головы. Поэтому и у нас будет туловище и голова. Для реализации выбранной схемы не нужно даже делать никаких автоматов.
Интерфейс модуля будет таким:
module AnalyzerHead (
input clk,
input reset,
input logic source_ready,
output logic source_valid,
output logic[63:0] source_data,
input logic [31:0] channels
);
Вот так мы реализуем процесс, который защёлкивает данные в регистрах и увеличивает счётчик:
logic [31:0] counter = 0;
logic [31:0] channels_D1 = 0;
logic [31:0] channels_D2 = 0;
always @ (posedge clk, posedge reset)
if (reset == 1)
begin
counter <= 0;
channels_D1 <= 0;
channels_D2 <= 0;
end else
begin
channels_D1 <= channels;
channels_D2 <= channels_D1;
counter <= counter + 1;
end
Первое условие записи:
logic valid1;
…
// Вариант срабатывания 1 - разные данные
assign valid1 = (channels_D1==channels_D2)?0:1;
Второе условие записи:
logic valid2;
// Вариант срабатывания 2 - переполнение счётчика
assign valid2 = (counter == 0)?1:0;
Результирующее условие записи:
// Сводим оба варианта воедино
// Если FIFO не готово - увы, данные пропадут
// В полноценном анализаторе надо зажигать аварию при этом
// тут - ну пропадут и пропадут...
assign source_valid = (valid1 | valid2) & source_ready;
Ну, и из опытов ясно, что байты на шине надо немного перекрутить:
// Данные вот так вот вывернуты
assign source_data [63:56] = counter [7:0];
assign source_data [55:48] = counter [15:7];
assign source_data [47:40] = counter [23:16];
assign source_data [39:32] = counter [31:24];
assign source_data [31:24] = channels_D1 [7:0];
assign source_data [23:16] = channels_D1 [15:7];
assign source_data [15:8] = channels_D1 [23:16];
assign source_data [7:0] = channels_D1 [31:24];
Собственно, всё. Давайте для полноты картины я вставлю полный текст модуля в слитном варианте.
Полный текст модуля
module AnalyzerHead (
input clk,
input reset,
input logic source_ready,
output logic source_valid,
output logic[63:0] source_data,
input logic [31:0] channels
);
logic [31:0] counter = 0;
logic [31:0] channels_D1 = 0;
logic [31:0] channels_D2 = 0;
logic valid1;
logic valid2;
always @ (posedge clk, posedge reset)
if (reset == 1)
begin
counter <= 0;
channels_D1 <= 0;
channels_D2 <= 0;
end else
begin
channels_D1 <= channels;
channels_D2 <= channels_D1;
counter <= counter + 1;
end
// Вариант срабатывания 1 - разные данные
assign valid1 = (channels_D1==channels_D2)?0:1;
// Вариант срабатывания 2 - переполнение счётчика
assign valid2 = (counter == 0)?1:0;
// Сводим оба варианта воедино
// Если FIFO не готово - увы, данные пропадут
// В полноценном анализаторе надо зажигать аварию при этом
// тут - ну пропадут и пропадут...
assign source_valid = (valid1 | valid2) & source_ready;
// Данные вот так вот вывернуты
assign source_data [63:56] = counter [7:0];
assign source_data [55:48] = counter [15:7];
assign source_data [47:40] = counter [23:16];
assign source_data [39:32] = counter [31:24];
assign source_data [31:24] = channels_D1 [7:0];
assign source_data [23:16] = channels_D1 [15:7];
assign source_data [15:8] = channels_D1 [23:16];
assign source_data [7:0] = channels_D1 [31:24];
endmodule
Упаковка головы в компонент для процессорной системы
Как-то зловеще звучит заголовок… Но как бы там ни было, а упаковать всё в компонент нам надо. Мы тренировались делать подобное в этой статье.
У меня получилась шина AVALON_ST, штатные линии тактирования и сброса, и… Но сначала рисунок с типовыми вещами:
Из нетиповых: для будущей задумки линии conduit пришлось дать осознанное имя типу сигнала. Оно нам ещё пригодится.
В остальном – вроде, всё понятно.
Проектируем процессорную систему
Как мы уже рассматривали в этой статье, мы не станем добавлять в систему процессорное ядро Nios II, а воспользуемся блоком Altera JTAG-to-Avalon-MM.
Работать с контроллером SDRAM мы учились в этой статье, а в этой разбирались, как при помощи блока PLL разогнать систему до 100 Мгц. Экспериментировали с FIFO и изменением ширины шины AVALON_ST при помощи блока AVALON_ST_ADAPTER мы в этой статье. Наконец, с DMA мы экспериментировали буквально в прошлой статье.
Пришла пора собрать все эти знания в едином проекте! Вот такая у меня получилась навёрнутая структурная схема.
Страшно? Ничуть. Давайте пройдёмся по ней сверху вниз. Сначала идёт блок тактирования и сброса. Как всегда, для комплекса Redd, чтобы не мучиться, физическую ножку Reset я не использую (я её всегда виртуальной делаю). Так удобнее для данной конкретной аппаратуры, хоть и не совсем правильно. Тактирование же идёт на блок PLL. Как его настраивать, мы уже подробно рассматривали раньше. Если я вставлю сюда массу скриншотов, то сильно перегружу статью. С выхода c0 мы берём тактовый сигнал для всей нашей системы, а выход c1 – экспортируем и подключаем к тактовому входу микросхемы SDRAM.
Master0 – это тот самый компонент Altera JTAG-to-Avalon-MM, через который мы будем достукиваться до шины AVALON_MM. Он в настройках не нуждается. Доступ к шине нам нужен, чтобы управлять блоком DMA и чтобы считывать содержимое SDRAM с накопленными результатами.
Дальше идёт наш компонент «Голова». А уже из неё растекается поток через цепочку шин AVALON_ST. Сначала он затекает в блок FIFO. Это – первый блок, настройки которого стоит показать особо:
8 символов на слово, каждый символ 8 бит. Итого 8*8=64 бита. Ёмкость – 4 килослова. Все остальные вещи протокола AVALON_ST отключены. Двойное тактирование сделано для того, чтобы в будущем голова могла работать на частоте, отличной от частоты работы туловища. Это нам пригодится, когда мы будем делать шинный анализатор USB.
Дальше данные перетекают в преобразователь разрядности. Вот его настройки:
Собственно, 8 символов на слово на входе, 4 символа на слово – на выходе. 8 бит на символ. Тоже всё просто. Наконец, поток входит в блок DMA. Ему я только типы шин и максимальную длину передачи поправил, да выставил режим доступа только в режиме полного слова, чтобы поднять Fmax. По уму, такой огромный объём памяти дескрипторов не нужен (хотя, нутром чую, что через них мы можем реализовать кольцевой буфер для анализатора). Размер входного FIFO тоже можно уменьшить до минимума, ведь у нас есть FIFO до этого блока. Но, честно говоря, работа над статьёй и так уже затянулась, так что оставим эту оптимизацию для читателей в качестве самостоятельной работы.
Всё. Потоковая часть завершена. Дальше данные попадают в контроллер SDRAM. Напомню его настройки
Из функциональной части – всё. Но кто следит за рассказом не по диагонали, а внимательно, наверное, заметил ещё один странный блок DataGen_0. Что это такое? Мы раньше такого не применяли!
Дело в том, что мне же как-то надо проверить работу головы. А это надо все 32 линии назначить на какие-то ножки ПЛИС, подключить к ним какой-то источник… А все должны будут поверить мне на слово, что я это сделал. И потом думать, как это повторить у себя. Зачем? Давайте добавим тестовый генератор данных и подключим его не проводами, а через трассировочные ресурсы ПЛИС. Я сделал самый простой счётчик, который увеличивает своё значение в случайные моменты времени. В качестве генератора случайных чисел я взял 32-разрядную M-последовательность, а увеличиваю счётчик, когда в младших восьми битах появляется константа 0x12. Вот такой получился SystemVerilog код, реализующий эту функциональность (обратите внимание, что я по-прежнему не использую сигнал reset, хотя, здесь бы он пригодился):
module DataGen
(
input clk,
output logic [31:0] data = 0
);
// Генератор случайных чисел
logic [31:0]shift_reg = 0;
logic next_bit;
assign next_bit = shift_reg[31] ^ shift_reg[30]
^ shift_reg[29] ^ shift_reg[27] ^ shift_reg[25]
^ shift_reg[ 0];
always @(posedge clk)
if(shift_reg == 0)
shift_reg <= 32'h12345678;
else
shift_reg <= { next_bit, shift_reg[31:1] };
// Целевой счётчик
always @(posedge clk)
begin
if (shift_reg [7:0] == 8'h12)
data <= data + 1;
end
endmodule
В настройках компонента самая важная деталь – это имя параметра Signal Type у conduit шины. Он должен быть таким же, какой я заполнил у соответствующего параметра головы. В остальном – всё просто, здесь же нет никаких специальных шин, только conduit…
Соединяем соответствующие линии (это единственное соединение на структурной схеме выше, которое я не стал подсвечивать каким-либо цветом), получаем то, что нужно.
Финал работ
Делаем линию reset виртуальной. Всем остальным ножкам я предпочёл сделать назначение не в GUI, а скопировал фрагмент файла *.qsf из проекта, сделанного в самой первой статье.
Вот этот фрагмент:
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to clk_clk
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[12]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[11]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[10]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[9]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[8]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[7]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[6]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[5]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[4]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[3]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[2]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[1]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_addr[0]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_ba[1]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_ba[0]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_cas_n
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_clk_clk
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_cs_n
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[15]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[14]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[13]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[12]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[11]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[10]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[9]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[8]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[7]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[6]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[5]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[4]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[3]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[2]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[1]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dq[0]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dqm[1]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_dqm[0]
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_ras_n
set_instance_assignment -name IO_STANDARD "3.3-V LVCMOS" -to sdram_we_n
set_location_assignment PIN_64 -to sdram_addr[12]
set_location_assignment PIN_60 -to sdram_addr[11]
set_location_assignment PIN_44 -to sdram_addr[10]
set_location_assignment PIN_59 -to sdram_addr[9]
set_location_assignment PIN_58 -to sdram_addr[8]
set_location_assignment PIN_55 -to sdram_addr[7]
set_location_assignment PIN_54 -to sdram_addr[6]
set_location_assignment PIN_53 -to sdram_addr[5]
set_location_assignment PIN_52 -to sdram_addr[4]
set_location_assignment PIN_51 -to sdram_addr[3]
set_location_assignment PIN_50 -to sdram_addr[2]
set_location_assignment PIN_49 -to sdram_addr[1]
set_location_assignment PIN_46 -to sdram_addr[0]
set_location_assignment PIN_73 -to sdram_dq[15]
set_location_assignment PIN_72 -to sdram_dq[14]
set_location_assignment PIN_71 -to sdram_dq[13]
set_location_assignment PIN_70 -to sdram_dq[12]
set_location_assignment PIN_69 -to sdram_dq[11]
set_location_assignment PIN_68 -to sdram_dq[10]
set_location_assignment PIN_67 -to sdram_dq[9]
set_location_assignment PIN_66 -to sdram_dq[8]
set_location_assignment PIN_30 -to sdram_dq[7]
set_location_assignment PIN_28 -to sdram_dq[6]
set_location_assignment PIN_11 -to sdram_dq[5]
set_location_assignment PIN_10 -to sdram_dq[4]
set_location_assignment PIN_7 -to sdram_dq[3]
set_location_assignment PIN_3 -to sdram_dq[2]
set_location_assignment PIN_2 -to sdram_dq[1]
set_location_assignment PIN_1 -to sdram_dq[0]
set_location_assignment PIN_65 -to sdram_dqm[1]
set_location_assignment PIN_31 -to sdram_dqm[0]
set_location_assignment PIN_34 -to sdram_ras_n
set_location_assignment PIN_32 -to sdram_we_n
set_location_assignment PIN_42 -to sdram_cs_n
set_location_assignment PIN_33 -to sdram_cas_n
set_location_assignment PIN_38 -to sdram_ba[0]
set_location_assignment PIN_39 -to sdram_ba[1]
set_location_assignment PIN_25 -to clk_clk
set_location_assignment PIN_43 -to sdram_clk_clk
set_instance_assignment -name VIRTUAL_PIN ON -to sdram_cke
Можно приступать к экспериментам… Но все уже устали, так что практикой мы займёмся в следующий раз.