OVM/Advanced OVM/Session1 - Understanding TLM
Материал из Wiki
Здравствуйте, добро пожаловать в Академию верификации. Я Том Фицпатрик, инженер по верификации из компании Mentor Graphics. В этом блоке мы поговорим о некоторых специальных вопросах OVM и немного о UVM. Первое занятие называется «Основы TLM». TLM означает «моделирование на уровне транзакций». Это фундамент, на котором строятся все механизмы повторного использования и модульности в OVM, поэтому очень важно, чтобы вы ясно представляли, как это работает.
Принцип работы TLM в OVM - коммуникация посредством вызова методов. Так называемые порты представляют собой API, используемый в ходе коммуникации. На показанном слайде класс initiator сверху вызывает метод put, в данном случае put_port, а некий другой компонент экспортирует реализацию этого метода. Таким образом, когда initiator вызывает put_port.put, фактически вызывается реализация, предоставленная компонентом target. Затем мы соединяем оба компонента, причем соединение производится между портом и экспортером, а не между самими компонентами. Поэтому функция connect в середине соединяет метод put_port инициатора с методом put_export целевого компонента target. Транзакции, то есть то, что передается через эти интерфейсы, суть объекты. Они расширяют класс ovm_transaction или, как мы увидим ниже, класс ovm_sequence_item. Эти транзакции передаются из порта экспортеру. Порты и экспортеры на самом деле параметризованы типом передаваемой транзакции. Определив компоненты, играющие роль портов и экспортеров, и соединив порты с экспортерами, можно приступать к передаче транзакций от одного компонента другому. Транзакция – это способ инкапсуляции той информации, которую вы считаете необходимым передавать между двумя компонентами. Это могут быть транзакции шины, адресные данные, поля состояния и т.п. При работе с протоколом Ethernet это может быть пакет, заголовок, полезная нагрузка и другие подобные вещи. В общем, любая информация, относящаяся к конкретному приложению. Порты и экспортеры параметризованы типом транзакции, которая передается между ними. Таким образом, смысл этих TLM-интерфейсов в том, что они позволяют заменить один компонент другим; в данном случае компонент target можно заменить другим компонентом, расширяющим target.
На слайде мы назвали его target2. При условии, что интерфейсы одинаковы, все соединения, описанные в родительском окружении, остаются действительными. Таким образом, мы изменили тип target2, воспользовавшись фабрикой (о них мы будем говорить на следующем занятии), но окружение настолько гибко, что такое изменение можно произвести не меняя код самого окружения. Это возможно, потому что соединяются интерфейсы port и export двух компонентов, а не сами компоненты. Поэтому, если объект, представленный target2, имеет такие же интерфейсы, то соединение будет по-прежнему работать. Это обеспечивает модульность, которая понадобится нам в OVM, чтобы воспользоваться другими средствами повышения гибкости, с которыми мы встретимся далее.
OVM поддерживает несколько типов TLM-интерфейсов. На данном слайде показаны односторонние интерфейсы. Блокирующие методы представляют собой задачи. Метод put отправляет транзакцию от одного компонента другому. Метод get или peek принимает транзакцию, отправленную другим компонентом. Мы будем обозначать порты квадратиками, а экспортеров кружочками. Таким образом, поток управления направлен от порта в сторону экспортера. Порт вызывает метод, а экспортер предоставляет реализацию. Поток же данных следует в направлении стрелки. Следовательно, в случае метода put данные передаются от порта к экспортеру, а в случае get - от экспортера к порту. Имеются также неблокирующие методы, представленные функциями с именами try_put, try_get и try_peek. Итак, put отправляет транзакцию, get возвращает транзакцию и удаляет ее из того места, откуда она была отправлена. Метод peek просто возвращает копию транзакции, оставляя ее саму на месте.Метод write - также функция, которая, как мы вскоре увидим, применяется для анализа коммуникаций. Функции try_put и try_get/try_peek возвращают статус, позволяющий узнать, завершилось ли выполнение успешно или нет. Все они возвращают управление немедленно, как любые функции в SystemVerilog. Существуют также двусторонние интерфейсы. Имеется интерфейс Master, который отправляет запрос и получает назад ответ. И интерфейс Slave, который делает все наоборот, то есть получает запрос и отправляет назад ответ. Имеется интерфейс Transport с двумя методами. Метод transport - это по существу вызов put_request, за которым следует get_response, но в виде единственного вызова метода. Он не возвращает управление, пока не будет получен ответ. Есть также неблокирующий вариант nb_transport, который пытается отправить запрос и получить ответ в надежде, что это произойдет немедленно. Если ответ еще не готов, то функция nb_transport возвращает статус 0. И, наконец, есть взаимодействие между последовательностью и драйвером, при котором драйвер получает запрос и отправляет ответ. У этой семантики есть ряд особенностей, которые мы рассмотрим на следующем занятии. Из-за них этот интерфейс, внешне похожий на Slave, на самом деле несколько от него отличается.
Я уже упоминал о методе write, который нужен для анализа коммуникаций. Если в системе имеется монитор, который опрашивает состояние шины, распознав транзакцию, то он может переправить эту транзакцию другим частям системы, компоненту проверки или сборщику покрытия. Важно, что это происходит мгновенно, поскольку монитор должен быть сразу же готов к появлению следующей транзакции. Поэтому порт анализа монитора можно соединить с несколькими так называемыми подписчиками. Когда кто-то обращается к функции write монитора, вызываются функции write всех подписчиков, и все происходит за нулевое время. Реализацию метода write предоставляет сам подписчик. Существует компонент ovm_subscriber, параметризуемый типом получаемой им транзакции. В него уже встроен экспортер analysis_export, и вам остается только расширить этот компонент, предоставив реализацию метода write. Порт анализа обозначается ромбом, и к нему можно присоединить один или несколько экспортеров. Как я уже отмечал, при вызове метода write монитора вызываются реализации write всех присоединенных к порту монитора экспортеров.
Соединения в TLM организованы иерархически. При построении системы из небольших компонентов возникает необходимость в передаче информации по иерархии. Простейшее соединение связывает порт с экспортером, в данном случае класс P1 с классом P2. Мы собираемся соединить метод put_port класса P1 с методом put_export класса P2. Синтаксически это записывается в виде вызова port.connect, которому передается экспортер. В середине слайда показано, что при вызове метода connect мы соединяем метод put_port P1 с методом export P2. Таким образом, экспортер всегда является аргументом, а вызывается метод connect порта. В случае иерархических соединений, если имеется потомок внутри P1, то необходимо соединить порт этого потомка с портом родителя, так что C1 в конечном итоге добирается до экспортера за пределами родителя. В данном случае мы вызываем метод child.port. connect родительского порта. Если угодно, можно сказать, что аргумент - это всегда объект, ближайший к экспортеру. Поэтому мы вызвали метод connect порта потомка и передали ему в качестве аргумента родительский порт. С другой стороны, если бы имелся еще один потомок внутри P2, то надо было бы соединить экспортера его родителя с экспортером потомка. То есть вызвать parent_export.connect, передав child.export в виде аргумента. Еще раз подчеркнем, что фактическую реализацию предоставляет потомок, поэтому он и является аргументом, и мы вызываем parent.export.connect в качестве того, что соединяется. Теперь получилось, что порт потомка P1 соединен с экспортером, предоставленным потомков P2, и эти иерархические соединения установлены с помощью метода connect. Все эти методы автоматически вызываются системой OVM. После того, как все соединения определены, в конце фазы elaboration мы проверяем и разрешаем все соединения, убеждаясь в том, что все правильно. Таким образом, к моменту, когда вызывается post_elaboration() все соединения в системе, определенные с помощью метода connect, уже проверены, все объекты соединены и можно запускать выполнение.
Сформулируем ряд правил, касающихся портов и экспортеров. Каждый порт должен быть в конечном итоге соединен с какой-то реализацией, которую мы для краткости называем imp. Это и есть фактический метод. Большинство необходимых вам реализаций встроены в базовые классы. Так, я уже говорил, что реализация порта анализа находится в классе ovm_subscriber В классе ovm_sequencer имеется реализация вытягивания элемента последовательности, а в классе tlm_fifo - реализации блокирующих и неблокирующих интерфейсов put, get и peek. Иногда возникает необходимость в создании собственной реализации, и мы рассмотрим примеры нескольких ситуаций, когда это может понадобиться.
Если требуется создать собственную реализацию, то к нашим услугам компонент my_comp. В нем есть свой конструктор. Это класс, как все вообще в OVM. Первым делом вы должны объявить необходимую вам реализацию. В данном случае мы имеем тип ovm_put_imp, в котором есть методы get_put, get_peek и прочие. Этот тип параметризован двумя параметрами. Один из них - тип транзакции, то есть объекта, с которым он будет работать. Второй - тип родителя, то есть класса, членом которого является данная реализация. Это не что иное, как пример применения объектно-ориентированного паттерна «Обертка». На слайде мы видим класс, названный put_export. Это и есть реализация, которая позволяет переадресовать вызов некоему методу, определенному внутри компонента. Как видим, мы объявили реализацию, у нее есть два параметра: тип транзакции и тип родителя. Далее мы должны объявить реализацию метода. В данном случае речь идет о классе put_imp, поэтому нужно определить реализацию метода put, которая параметризована тем же самым типом транзакции T. Это именно тот метод, который будет вызван когда порт вызовет метод put данного компонента посредством соединений, которые ведут к реализации put в целевом классе. Нам осталось только сконструировать реализацию. Поскольку реализация сама является классом, ее необходимо конструировать. В методе build компонента мы вызываем конструктор экспортера. В данном случае мы собираемся воспользоваться именно данной реализацией и знаем, что будем ей пользоваться и изменять ничего не потребуется. Поэтому мы можем вызвать конструктор напрямую, не прибегая к фабрике. О фабриках мы будем говорить на следующем занятии. Итак, нам необходимо объявить реализацию правильно параметризовать ее, определить метод, который предстоит вызывать, а затем сконструировать экспортер, вызвав new в методе компонента build. После этого все готово к работе. Теперь этот компонент готов предоставить реализацию метода put любому компоненту, с которым мы его свяжем, - посредством put_export.
К числу наиболее употребительных при верификации компонентов относится компонент проверки. Как правило, он соединяется с двумя другими компонентами, которые посылают ему транзакции, которые нужно тем или иным способом сравнить (обычно ожидаемую и фактическую). Компонент проверки проверяет, что произошло именно то, что ожидалось, то есть обе транзакции совпадают. Что конкретно делает компонент проверки, сильно зависит от приложения но сам механизм достаточно универсален. Мы хотим создать подобный компонент проверки, в котором будет два экспортера для анализа: для ожидаемой и для фактической транзакции. Проблема в том, что в одном компоненте можно реализовать лишь один метод write. Мы обойдем эту сложность, создав внутри компонента проверки два подкомпонента. Они будут объектами класса analysis_fifo. Класс analysis_fifo расширяет tlm_fifo, добавляя реализацию метода write. Количество объектов в очереди analysis_fifo не ограничено. Ожидаемые транзакции хранятся в одном объекте fifo, а фактические рано или поздно окажутся в другом. Поскольку размер очереди не ограничен, о переполнении можно не беспокоиться. В методе run компонент проверки будет извлекать объекты из каждой очереди и сравнивать фактический результат с ожидаемым. В данном случае мы используем метод get в виде блокирующей задачи. Сначала мы получаем ожидаемый результат, а затем фактический (возможно, не сразу). Это сравнение по порядку. Есть и другие способы, позволяющие сравнивать транзакции, поступающие не по порядку. Но здесь мы в такие детали вдаваться не будем. Итак, мы имеем активный компонент проверки, который сам извлекает транзакции из двух очередей.
Но возможна и альтернативная реализация компонента проверки, больше напоминающая пассивное устройство. В таком случае мы могли бы включить методы write непосредственно в компонент проверки. Эти методы могли бы, например, активировать группы покрытия или делать еще что-то. Для этого нужно будет воспользоваться классом ovm_subscriber из библиотеки OVM. Он параметризован типом транзакции, и нам остается лишь расширить ovm_subscriber, предоставив реализацию метода write. Создадим два подписчика, каждый из которых будет вызывать один из вариантов метода write родителя. Здесь m_parent - встроенный указатель на родительский компонент, которым в данном случае является компонент проверки. Мы соединяем экспортеры анализа подписчиков с экспортерами анализа компонента проверки точно так же, как делали для analysis_fifo. Теперь всякий раз, как вызывается метод write любого из этих экспортеров, будет выполнен вызов соответствующего метода write родительского компонента проверки. Это позволяет организовать компонент проверки, который можно далее расширить, получив любое число экспортеров анализа и тем самым собрать такое окружение, какое нам необходимо. Теперь, какие бы компоненты ни вошли в систему, генератор стимулов будет порождать транзакции и передавать их другому компоненту. Могут присутствовать другие компоненты, получающие транзакции от иных объектов. Все основано на использовании описанных TLM-интерфейсов. Как мы видим, окружение соединяет их между собой, а, когда это сделано, встроенные в OVM гибкие механизмы позволяют заменять один компонент другим, имеющим такие же интерфейсы. При этом код установления соединений в окружении не изменяется. Мы еще будем говорить об этом на последующих занятиях, но уже сейчас отметим, что использование TLM-интерфейсов для создания замкнутых компонентов и определения семантики коммуникации между любым компонентом и тем, что с ним соединено, дает ту гибкость, которая является одной из основных конструктивных особенностей OVM. Спасибо за внимание.