OVM/Advanced OVM/Session2 - Understanding the Factory
- Understanding TLM
- Understanding the Factory
- The Care and Feeding of Sequences
- Layering Sequences
- Writing and Managing Tests
Здравствуйте, я Том Фицпатрик, инженер по верификации в компании Mentor Graphics. Мы начинаем занятие по применению OVM и UVM. Тема занятия – "Использование паттерна Фабрика".
В OVM все создаваемые компоненты и транзакции являются объектами. Чтобы выделить для объекта память и начать использовать его, объект нужно сконструировать. Обычно объекты конструируются в методе build. Если выбрать для стандартного конструктора SystemVerilog имя new, то мы получим объект указанного типа и выделим для него память. В данном случае объект comp1 имеет тип my_comp, и этот тип сохранится за ним навсегда. Этот тип зашит в коде окружения, показанном на слайде. Однако для достижения большей гибкости мы предпочитаем – и OVM это позволяет - вызывать не конструктор,а метод create. Этот стандартный паттерн объектно- ориентированного программирования называется Фабрика.Метод create возвращает экземпляр сконструированного объекта. В данном случае при вызове метода create от имени comp2, мы получаем экземпляр my_comp с именем comp2. Он во всем аналогичен любому другому экземпляру и, в частности, созданному выше объекту comp1, но благодаря использованию фабрики мы достигаем чуть большей гибкости. В действительности фабрика позволяет подменить тип component2 любым другим типом, расширяющим базовый тип my_comp. Как видите, код окружения ничуть не изменился. Он по-прежнему создает экземпляр comp2, но благодаря изменению фабрики тип возвращаемого объекта теперь стал my_Xcomp вместо my_comp. Ниже мы гораздо более подробно рассмотрим, как это работает в OVM, но основная идея ясна: в сочетании с использованием TLM вы сможете подменить любой компонент другим компонентом похожего типа. Благодаря применению фабрики код окружения при этом вообще не меняется. Он по-прежнему создает экземпляр компонента, причем метод окружения connect также не изменяется, потому что соединяет одни и те же интерфейсы компонентов.
Итак, чтобы воспользоваться фабрикой, первым делом необходимо зарегистрировать в фабрике компонент или объект. Для объектов, то есть, например, транзакций и элементов последовательности, имеется макрос ovm_object_utils, который скрывает детали фактической регистрации в фабрике. Для компонентов нужно проделать чуть больше работы, поэтому предлагается другой макрос ovm_component_utils. Итак, первое, что нужно сделать при создании собственного компонента или транзакции, – с помощью макроса utils зарегистрировать его в фабрике. Обратите внимание, что, поскольку это макрос, точка с запятой в конце строки не ставится.
Теперь класс my_comp зарегистрирован в фабрике, и макрос создает обертку, называемую type_id. А чтобы создать экземпляр comp2, мы обращаемся к type_id – статическому члену типа, созданного макросом. Поэтому мы пишем my_comp (требуемый тип) ::type_id, (статическая обертка), а в этой обертке имеется статический метод, который тоже называется create. Итак, мы записываем::create, а затем указываем имя экземпляра и родителя, точно так же, как при конструировании объекта. Но теперь именно метод create обертки в фабрике возвращает экземпляр my_comp, который мы присваиваем comp2. Таким образом, create возвращает экземпляр типа и заметьте, что нам не пришлось использовать знак доллара для приведения, потому что всюду имеет место строгая типизация, так как мы пользуемся статическими методами и статическими членами нужного нам типа, который мы явно задали. Следовательно, метод create автоматически возвращает экземпляр подходящего типа. Существует также метод get_type, который возвращает что-то вроде описателя типа. Это не настоящие описатели, такие, как в SystemVerilog, тем не менее возвращаемое значение можно использовать как описатель типа в других операциях с фабрикой, и вскоре мы в этом убедимся. Дополнительно создаются еще два метода с именами set_type_override и set_inst_override. С их помощью мы можем попросить фабрику переопределить тип объекта, возвращаемого методом create. Как это используется, мы увидим на последующих слайдах.
Рассмотрим пример переопределения типов в OVM. Здесь мы имеем класс окружения, который создает два экземпляра типа shape: u1 и u2. По умолчанию shape выглядит как золотистый квадрат. В методе окружения build мы вызываем метод create типа shape и получаем два экземпляра, u1 и u2, типа shape. В нашем тесте мы создадим окружение в методе build. А затем в том же методе build зададим фабрику, которая будет переопределять некоторые типы. Мы собираемся воспользоваться методом set_type_override типа shape, поэтому пишем shape::type_id::set_type_override и передаем тип circle. Именно здесь мы используем метод get_type. В результате возвращается обертка типа circle. Когда окружение попытается создать объект типа shape, оно на самом деле получит объект типа circle. Таким образом, u1 и u2 – круги, а не квадраты. Обратите внимание, что код окружения не изменился. Можно также переопределить конкретный экземпляр. Здесь мы пользуемся методом shape::type_id::set_inst_override и хотим, чтобы конкретный экземпляр e.u2 имел тип triangle. Теперь экземпляр u2 в окружении будет треугольником.Отметим, что сначала мы подменили тип shape на circle, а затем для экземпляра u2 произвели еще одну подмену, указав тип triangle. Таким образом, переопределение экземпляра имеет более высокий приоритет, чем типа. Итак, без внесения каких-либо изменений в код окружения мы получили экземпляр круга и экземпляр треугольника. При условии, что типы circle и triangle расширяют базовый тип shape, такого рода операции возможны.
Далее мы хотели бы уметь работать с параметризованными типами. Сейчас мы добавим в окружение экземпляр u3, который будет иметь тип red, параметризованный числом сторон, причем по умолчанию мы хотим здесь получить четыре стороны. Для регистрации в фабрике параметризованного типа применяется макрос ovm_component_param_utils, которому передается специализация, или тип с заданным параметром value. Итак, по умолчанию в качестве типа red будет выступать triangle, т.е. число сторон равно 3, но в данном случае при инстанцировании мы задаем параметр value равным четырем, так что окружение создаст красный квадрат в качестве u3 и вызовет фабрику именно с такой параметризацией типа red. Стало быть, мы пишем red #4, а затем вызываем type_id::create для получения экземпляра u3.
А при переопределении в тесте мы снова применим параметризацию. То есть напишем red#(4)::type_id::set_type_override, и передадим ему blue 4. Итак, при использовании в фабрике параметризованного типа параметризации должны соответствовать друг другу. Тип blue должен расширять тип red, и специализация фактического параметра применяется при обращении к методам set_type_override и get_type. Теперь, ничего не изменив в коде окружения мы получили в объекте U3 синий квадрат вместо красного. Таким образом, фабрику можно использовать как с обычными, так и с параметризованными типами. Следует также помнить, что окружения сами являются компонентами, поэтому при инстанцировании окружения в тесте мы должны вызывать для него метод create, потому что, как мы увидим ниже, иногда в тесте желательно переопределить тип инстанцируемого окружения. Поэтому всякий раз при инстанцировании окружения важно вызывать метод create самого типа environment.
В OVM тесты тоже являются компонентами, поэтому их следует регистрировать в фабрике. Следовательно, каждый тест нужно зарегистрировать с помощью макроса ovm_component_utils, А затем в модуле верхнего уровня вызывается метод run_test. Рекомендуется вызывать run_test без аргументов, чтобы имя теста можно было указать в командной строке в виде аргумента + OVM test name. Если метод run_test вызван без аргументов, то пользователь будет вынужден указать имя теста в командной строке; мы рекомендуем именно такой стиль. Тогда, если вы забудете указать имя теста в командной строке, то система сообщит об ошибке.
В фабрике может быть зарегистрировано много тестов. Вы указываете в командной строке, какой из них хотите прогнать, и метод run_test обращается к фабрике для создания экземпляра теста указанного типа. Надо только помнить, что, поскольку при таком использовании фабрики в качестве типа компонента задаются просто строки, сами тесты невозможно параметризовать, так как фабрика не поддерживает параметризованные классы с именами, заданными в виде строк. Мы еще вернемся к этому вопросу ниже.
Хотелось бы также уметь применять фабрики к элементам последовательности. Пусть контроллер генерирует последовательность транзакций, отправляемых драйверу для подачи на вход вашего устройства. Иногда бывает полезно использовать фабрику для переопределения элемента. Например, фабрика могла бы создать транзакцию ошибки вместо базовой транзакции или еще что-то в этом роде. Поэтому внутри последовательности (и мы еще вернемся к этой теме на следующем занятии) при фактическом создании транзакции мы будем вызывать метод create для различных типов транзакций, то есть писать my_transaction (базовый тип транзакции) ::type_id::create, и передавать имя транзакции. Но, поскольку это элемент последовательности, он не принадлежит иерархии компонентов. Поэтому мы оставляем следующий аргумент пустым. То есть для данного конкретного элемента последовательности мы не задаем родителя, а для получения третьего аргумента вызываем метод get_fullname, который возвращает наше местоположение в общей иерархии. Это называется контекстом и позволяет фабрике найти данную конкретную транзакцию, на случай, если мы захотим произвести только для нее переопределение на уровне экземпляра. Если нас интересует лишь переопределение транзакций на уровне типа, то эти два аргумента не используются, и о них можно забыть. Однако, чтобы можно было произвести переопределение экземпляра, необходимо в качестве контекста (третий аргумент) передавать get_fullname.
Затем, создав транзакцию, мы, как для любой последовательности, вызываем start_item. После возврата из start_item можно произвести рандомизацию, после чего вызвать метод finish item, который и отправит транзакцию драйверу. С последовательностью разобрались. Но мы хотим, чтобы тест умел переопределять тип фактически создаваемой транзакции.
Поэтому мы вызываем в тесте метод set_type_override от имени типа my_transaction. Теперь всякий раз при создании транзакции типа my_tr мы фактически будем создавать транзакцию ошибки, имеющую тип err_tr. Итак, создаем тип транзакции ошибки, расширяющий тип my_transaction, регистрируем его в фабрике но на этот раз используем ovm_object_utils, ибо имеем дело с объектами, а не с компонентами. Теперь, когда контроллер попытается создать транзакцию типа my_tr, фактически будет создана транзакция типа error. Это полезный способ настройки поведения теста во время исполнения последовательности без изменения кода самой последовательности. Можно также задать переопределение экземпляра для некоторой транзакции, так чтобы в случае, когда последовательность environment.agent.sequencer.sequence1 s1, пытается создать нечто с именем tx, производилась подмена типа для этой конкретной транзакции. Но если sequence1 создает также транзакции с именем rx, то они подменяться не будут, так что можно переопределять не только компоненты, но и отдельные экземпляры транзакций. Таким образом, у нас есть путь к контроллеру, имя последовательности s1, и имя генерируемого элемента. Это дает нам уникальный путь к конкретному элементу, генерируемому данной последовательностью, вне зависимости от того, в каком месте иерархии эта последовательность находится.
Стало быть, с помощью переопределения экземпляра мы можем изменять тип генерируемого элемента как для конкретной последовательности для заданного контроллера, так и для любого элемента с указанным именем, создаваемого последовательностью. Достигается это путем указания пути к контроллеру перед именем последовательности. Их можно комбинировать произвольно, и таким образом изменять любой элемент, генерируемый с помощью фабрики.
Подведем итог. Всегда используйте макросы ovm_object или ovm_component_utils для регистрации соответственно объектов или компонентов в фабрике. Помните, что после вызова макроса не должно быть точки с запятой. Для параметризованных объектов и компонентов применяйте макросы param_utils. Всегда вызывайте метод create, указывая имя создаваемого типа, а затем ::type_id::create. Это позволяет фабрике переопределять тип объекта, возвращаемого методом create. Не забывайте регистрировать в фабрике свои тесты, чтобы их можно было создавать из командной строки. При вызове метода run_test не задавайте аргумент, чтобы заставить пользователей указывать имя прогоняемого теста в командной строке с помощью аргумента OVM. Благодарю за внимание.