Как сделать AXI FIFO в блочной ОЗУ, используя готовое/действующее рукопожатие
Меня немного раздражали особенности интерфейса AXI, когда мне впервые пришлось создавать логику для интерфейса модуля AXI. Вместо обычных управляющих сигналов «занят/действителен», «полный/действителен» или «пустой/действителен» интерфейс AXI использует два управляющих сигнала с именами «готов» и «действителен». Мое разочарование вскоре сменилось благоговением.
Интерфейс AXI имеет встроенное управление потоком без использования дополнительных управляющих сигналов. Правила достаточно просты для понимания, но есть несколько подводных камней, которые необходимо учитывать при реализации интерфейса AXI на ПЛИС. В этой статье показано, как создать AXI FIFO в VHDL.
AXI решает проблему задержки на один цикл
Предотвращение перечтения и перезаписи является распространенной проблемой при создании интерфейсов потоков данных. Проблема в том, что при обмене данными между двумя синхронизируемыми логическими модулями каждый модуль сможет считывать выходные данные своего аналога только с задержкой в один тактовый цикл.
На изображении выше показана временная диаграмма последовательного модуля, записывающего в FIFO, который использует разрешение записи/полный сигнальная схема. Интерфейсный модуль записывает данные в FIFO, утверждая wr_en
сигнал. FIFO установит full
сигнал, когда нет места для другого элемента данных, побуждая источник данных прекратить запись.
К сожалению, интерфейсный модуль не может останавливаться во времени, пока он использует только синхронизированную логику. FIFO поднимает full
флаг точно на переднем фронте часов. Одновременно интерфейсный модуль пытается записать следующий элемент данных. Он не может сэмплировать и реагировать на full
сигнализируй, пока не поздно.
Одним из решений является добавление дополнительных almost_empty
сигнал, мы сделали это в учебнике Как создать кольцевой буфер FIFO в VHDL. Дополнительный сигнал предшествует empty
сигнал, давая интерфейсному модулю время среагировать.
Готовое/действительное рукопожатие
Протокол AXI реализует управление потоком, используя только два управляющих сигнала в каждом направлении, один из которых называется ready
. и другие valid
. ready
сигнал контролируется приемником, логический '1'
значение этого сигнала означает, что приемник готов принять новый элемент данных. valid
сигнал, с другой стороны, контролируется отправителем. Отправитель должен установить valid
до '1'
когда данные, представленные на шине данных, пригодны для выборки.
А вот и важная часть: передача данных происходит только тогда, когда оба ready
и valid
'1'
в том же такте. Получатель сообщает, когда он готов принять данные, а отправитель просто помещает данные туда, когда ему есть что передать. Передача происходит, когда оба согласны, когда отправитель готов отправить, а получатель готов получить.
На графике выше показан пример транзакции одного элемента данных. Выборка происходит по переднему фронту тактового сигнала, как это обычно бывает с тактовой логикой.
Реализация
Есть много способов реализовать AXI FIFO в VHDL. Это может быть регистр сдвига, но мы будем использовать структуру кольцевого буфера, потому что это самый простой способ создать FIFO в блочной ОЗУ. Вы можете создать все это в одном гигантском процессе, используя переменные и сигналы, или вы можете разделить функциональность на несколько процессов.
Эта реализация использует отдельные процессы для большинства сигналов, которые должны быть обновлены. Только процессы, которые должны быть синхронными, чувствительны к часам, остальные используют комбинационную логику.
Объект
Объявление объекта включает общий порт, который используется для установки ширины входных и выходных слов, а также количества слотов для резервирования места в ОЗУ. Емкость FIFO равна глубине оперативной памяти минус один. Один слот всегда остается пустым, чтобы различать полный и пустой FIFO.
09
Первые два сигнала в объявлении порта — это входы синхронизации и сброса. Эта реализация использует синхронный сброс и чувствительна к переднему фронту тактового сигнала.
Существует входной интерфейс в стиле AXI, использующий готовые/действительные управляющие сигналы и сигнал входных данных общей ширины. Наконец, появляется выходной интерфейс AXI с такими же сигналами, как и на входе, только с обратными направлениями. Сигналы, принадлежащие интерфейсу ввода и вывода, имеют префикс in_
. или out_
.
Выход одного AXI FIFO можно было подключить напрямую ко входу другого, интерфейсы идеально подходили друг к другу. Хотя лучшим решением, чем складывать их друг в друга, было бы увеличение ram_depth
универсальный, если вы хотите увеличить FIFO.
Объявления сигналов
Первые два оператора в декларативной области VHDL-файла объявляют тип RAM и его сигнал. Объем ОЗУ динамически изменяется на основе общих входных данных.
18
Второй блок кода объявляет новый целочисленный подтип и четыре сигнала от него. index_type
имеет размер, точно отражающий глубину оперативной памяти. head
сигнал всегда указывает слот ОЗУ, который будет использоваться в следующей операции записи. tail
signal указывает на слот, к которому будет осуществляться доступ при следующей операции чтения. Значение count
signal всегда равен количеству элементов, хранящихся в настоящий момент в FIFO, и count_p1
является копией того же сигнала, задержанного на один такт.
26
Затем идут два сигнала с именами in_ready_i
. и out_valid_i
. Это просто копии выводов сущности in_ready
и out_valid
. _i
постфикс просто означает внутренний , это часть моего стиля написания кода.
39
Наконец, мы объявляем сигнал, который будет использоваться для обозначения одновременного чтения и записи. Я объясню его назначение позже в этой статье.
48
Подпрограммы
После сигналов мы объявляем функцию для увеличения нашего пользовательского index_type
. next_index
функция смотрит на read
и valid
параметры, чтобы определить, есть ли текущая транзакция чтения или чтения/записи. В этом случае индекс будет увеличен или завернут. Если нет, возвращается неизменное значение индекса.
56
Чтобы избавить нас от повторного ввода, мы создаем логику для обновления head
и tail
сигналы в процедуре, а не как два идентичных процесса. update_index
процедура принимает часы и сигналы сброса, сигнал index_type
, ready
сигнал и valid
сигнал в качестве входных данных.
69
Этот полностью синхронный процесс использует next_index
функция для обновления index
сигнал, когда модуль вышел из состояния сброса. При сбросе index
signal будет установлено наименьшее значение, которое он может представлять, которое всегда равно 0 из-за того, как index_type
и ram_type
объявляется. Мы могли бы использовать 0 в качестве значения сброса, но я стараюсь максимально избегать жесткого кодирования.
Копировать внутренние сигналы на выход
Эти два параллельных оператора копируют внутренние версии выходных сигналов в фактические выходы. Нам нужно работать с внутренними копиями, потому что VHDL не позволяет нам читать сигналы объектов с режимом out
. внутри модуля. Альтернативой было бы объявить in_ready
и out_valid
с режимом inout
, но большинство стандартов кодирования компаний ограничивают использование inout
сигналы сущности.
70
Обновите голову и хвост
Мы уже обсуждали index_proc
процедура, которая используется для обновления head
и tail
сигналы. Сопоставляя соответствующие сигналы с параметрами этой подпрограммы, мы получаем эквивалент двух идентичных процессов, один для управления вводом FIFO и один для вывода.
87
Поскольку оба head
и tail
установлены на одно и то же значение логикой сброса, FIFO изначально будет пустым. Вот как работает этот кольцевой буфер, когда оба указывают на один и тот же индекс, это означает, что FIFO пуст.
Вычислить ОЗУ блока
В большинстве архитектур ПЛИС блочные примитивы ОЗУ являются полностью синхронными компонентами. Это означает, что если мы хотим, чтобы инструмент синтеза выводил блочную ОЗУ из нашего кода VHDL, нам нужно поместить порты чтения и записи внутрь синхронизированного процесса. Кроме того, не может быть значений сброса, связанных с блочной ОЗУ.
97
Нет разрешения чтения или разрешить запись здесь это было бы слишком медленно для AXI. Вместо этого мы непрерывно записываем в слот ОЗУ, на который указывает head
индекс. Затем, когда мы определяем, что произошла транзакция записи, мы просто продвигаем head
чтобы зафиксировать записанное значение.
Аналогично, out_data
обновляется каждый такт. tail
указатель просто перемещается на следующий слот, когда происходит чтение. Обратите внимание, что next_index
Функция используется для вычисления адреса порта чтения. Мы должны сделать это, чтобы убедиться, что оперативная память достаточно быстро реагирует после чтения и начинает выводить следующее значение.
Подсчитать количество элементов в FIFO
Подсчет количества элементов в оперативной памяти — это просто вычитание head
из tail
. Если head
завернуто, мы должны компенсировать это на общее количество слотов в оперативной памяти. У нас есть доступ к этой информации через ram_depth
константа из общего ввода.
105пре>Нам также нужно отслеживать предыдущее значение
count
. сигнал. Приведенный ниже процесс создает его версию с задержкой на один такт._p1
постфикс — это соглашение об именах, указывающее на это.112Обновите готово вывод
in_ready
сигнал должен быть'1'
когда этот модуль готов принять другой элемент данных. Так и должно быть до тех пор, пока FIFO не заполнен, и это именно то, о чем говорит логика этого процесса.127Обнаружение одновременного чтения и записи
Из-за крайнего случая, который я объясню в следующем разделе, нам необходимо иметь возможность идентифицировать одновременные операции чтения и записи. Каждый раз, когда в течение одного и того же тактового цикла выполняются действительные транзакции чтения и записи, этот процесс устанавливает
read_while_write_p1
сигнал на'1'
в следующем такте.132Обновить действительный вывод
out_valid
сигнал указывает нижестоящим модулям, что данные, представленные наout_data
действителен и может быть использован в любое время.out_data
сигнал поступает непосредственно с выхода RAM. Реализацияout_valid
сигнал немного сложен из-за дополнительной задержки тактового цикла между вводом и выводом блока ОЗУ.Логика реализована в комбинационном процессе, так что она может без задержки реагировать на изменяющийся входной сигнал. Первая строка процесса — это значение по умолчанию, которое устанавливает
out_valid
сигнал на'1'
. Это будет преобладающее значение, если ни одно из двух последующих операторов If не будет запущено.144Первый оператор If проверяет, пуст ли FIFO или был пуст в предыдущем такте. Очевидно, что FIFO пуст, когда в нем 0 элементов, но нам также необходимо проверить уровень заполнения FIFO в предыдущем такте.
Рассмотрим форму волны ниже. Первоначально FIFO пуст, что обозначается кодом
count
. сигнал0
. Затем запись происходит на третьем такте. Слот ОЗУ 0 обновляется в следующем такте, но требуется дополнительный цикл, прежде чем данные появятся наout_data
выход. Назначениеor count_p1 = 0
заявление, чтобы убедиться, чтоout_valid
остается'0'
(обведено красным), пока значение распространяется по ОЗУ.
Последний оператор If защищает от другого крайнего случая. Мы только что говорили о том, как справиться с особым случаем записи в пустой буфер, проверив текущий и предыдущий уровни заполнения FIFO. Но что произойдет, если и мы выполним одновременное чтение и запись, когда
count
уже1
?На графике ниже показана такая ситуация. Первоначально в FIFO присутствует один элемент данных D0. Он был там некоторое время, так что оба
count
иcount_p1
0
. Затем в третьем такте происходит одновременное чтение и запись. Один элемент покидает FIFO, а в него поступает новый, оставляя счетчики без изменений.
В момент чтения и записи в ОЗУ нет очередного значения, готового к выводу, как было бы, если бы уровень заполнения был выше единицы. Мы должны подождать два такта, прежде чем входное значение появится на выходе. Без какой-либо дополнительной информации было бы невозможно обнаружить этот угловой случай, и значение
out_valid
в следующем тактовом цикле (отмечен сплошным красным цветом) будет ошибочно установлено значение'1'
.Вот почему нам нужен
read_while_write_p1
сигнал. Он обнаруживает, что имели место одновременные операции чтения и записи, и мы можем принять это во внимание, установивout_valid
на'0'
в этом такте.Синтез в Vivado
Чтобы реализовать дизайн как автономный модуль в Xilinx Vivado, мы сначала должны задать значения для общих входных данных. Этого можно добиться в Vivado с помощью Настройки → Общие → Общие параметры/параметры меню, как показано на изображении ниже.
Общие значения были выбраны в соответствии с примитивом RAMB36E1 в архитектуре Xilinx Zynq, которая является целевым устройством. Использование ресурсов после внедрения показано на изображении ниже. AXI FIFO использует один блок ОЗУ и небольшое количество LUT и триггеров.
AXI более чем готов/действителен
AXI расшифровывается как Advanced eXtensible Interface и является частью стандарта ARM Advanced Microcontroller Bus Architecture (AMBA). Стандарт AXI — это намного больше, чем рукопожатие чтения/действия. Если вы хотите узнать больше об AXI, я рекомендую следующие ресурсы для дальнейшего чтения:
- Википедия:AXI
- Введение в ARM AXI
- Введение в Xilinx AXI
- Спецификация AXI4
VHDL
- Облако и как оно меняет мир ИТ
- Как максимально использовать свои данные
- Как инициализировать RAM из файла с помощью TEXTIO
- Как подготовиться к использованию ИИ с помощью Интернета вещей
- Как промышленный Интернет меняет управление активами
- Рекомендации по отслеживанию активов:как максимально использовать данные об активах, заработанных с трудом
- Как мы можем лучше понять Интернет вещей?
- Как максимально эффективно использовать Интернет вещей в ресторанном бизнесе
- Как данные позволяют использовать цепочку поставок будущего
- Как сделать данные цепочки поставок надежными