OVM/Basic OVM/Session6 - Introducing Transactions
Здравствуйте, я Джон Эйнсли из компании Дулос. Это шестое занятие по основам OVM, посвященное введению в транзакции. Я хочу познакомить вас с тем, как писать транзакции в OVM, но фактически транзакциями мы не ограничимся, предстоит обсудить также последовательности, представляющие собой компоненты OVM, которые генерируют транзакции, а также драйверы - компоненты, которые принимают транзакции и преобразуют их в сигналы на контактах тестируемого устройства.
На предыдущем занятии мы рассматривали структуру типичного компонента верификации и видели, что он состоит из контроллера, драйвера и монитора. Сейчас нас будет интересовать главным образом взаимодействие между контроллером и драйвером. Драйвер должен подавать сигналы на контакты тестируемого устройства, однако взаимодействие между контроллером и драйвером происходит в виде передачи транзакций, или абстрактных команд. Каждая транзакция – это отправляемая контроллером драйверу команда подвергнуть тестируемое устройство тому или иному воздействию. Весь смысл моделирования на уровне транзакций сводится к формальному представлению этого взаимодействия в виде отдельных транзакций с очень четко определенной структурой. Именно этим мы и займемся.
Но сначала покажем, какое место в этой картине занимает иерархия классов OVM, с которой мы познакомились на предыдущем занятии. Мы видели, что в OVM имеется целое иерархически организованное семейство классов для описания компонентов, то есть структурных элементов. Имеется также параллельная иерархия классов для описания транзакций, или данных. Базовым для всех транзакций является класс ovm_transaction. Последовательность, строго говоря, представляется классом, производным от ovm_sequence_item. Таким образом, класс ovm_sequence расширяет ovm_sequence_item, который в свою очередь расширяет ovm_transaction, ну а тот расширяет ovm_object. Транзакции и последовательности - это данные в противоположность компонентам, которые суть элементы структуры. Транзакции не являются частью иерархии компонентов. Если у компонента OVM обычно имеется родительский компонент, то у транзакций, передаваемых от одного компонента другому, родителей нет.
Давайте посмотрим, как можно определить в OVM свою транзакцию. Мы создадим пользовательский класс my_transaction, расширяющий ovm_sequence_item. Отметим, что мы расширяем класс ovm_sequence_item, а не ovm_transaction, потому что создаваемая транзакция, скорее всего, будет частью последовательности. Наследование от ovm_sequence_item вместо ovm_transaction как раз и позволяет транзакции быть частью последовательности. Поэтому в общем случае классы транзакций следует создавать путем расширения ovm_sequence_item.
Во второй строке нашего класса мы регистрируем его в качестве транзакции, применяя еще один стандартный макрос OVM. Обратите внимание, что мы используем ovm_object_utils, а не ovm_component_utils. Мы встречались с двумя стандартными макросами для регистрации классов:ovm_component_utils и ovm_object_utils. Крайне важно использовать правильный макрос.
Далее идут поля - свойства транзакции. В данном случае транзакция содержит команду, адрес и данные. Они и составляют свойства класса. Объявлению каждого свойства предшествует ключевое слово rand. В языке SystemVerilog слово rand означает, что после создания и рандомизации экземпляра данного класса значения объявленных таким образом свойств будут случайными. И это правильно, потому что мы собираемся использовать сгенерированные транзакции как стимулы для подачи на входы тестируемого устройства. Наконец, после объявления свойств мы видим первый пример ограничений SystemVerilog. Настоятельно рекомендуется включать такие ограничения в определение транзакции,чтобы ее свойства гарантированно имели осмысленные значения по умолчанию.Тогда, что бы ни происходило в конкретных тестах, если мы сгенерируем экземпляр класса my_transaction по умолчанию, что мы и собираемся сделать, наличие ограничений дает уверенность, что свойства такого экземпляра будут иметь разумные значения. Мы задали ограничения так, чтобы адрес и данные находились в допустимом диапазоне. Касательно ограничений стоит сделать одно важное замечание: поскольку это ограничения, а не процедурный код, впоследствии их можно будет переопределить. Стало быть, ограничения по природе своей не жесткие, вы всегда можете изменить решение. Пока что мы сказали, что адрес лежит в диапазоне от 0 до 256, и, значит, обычно он будет принадлежать этому диапазону. Но позже, если возникнет такая необходимость, мы сможем переопределить это ограничение, заменив его чем-то совершенно иным.
Далее следует конструктор, как во всяком пользовательском классе. У любого класса в языке SystemVerilog должен быть конструктор, и здесь мы включили в конструктор типичный трафаретный код. Однако отметим, что этот конструктор несколько отличается от конструктора класса, производного от ovm_component. Поскольку транзакция не является частью иерархии компонентов, у нее нет родительского компонента. Поэтому у конструктора отсутствует второй аргумент, parent. Это, конечно, очень простая транзакция, но ее уже достаточно для работы.
Итак, транзакцию мы определили, теперь перейдем к контроллеру. Контроллер - это стандартный компонент OVM, задача которого – генерировать случайную последовательность транзакций.
Для создания контроллера мы определим пользовательский класс, расширяющий ovm_sequencer - еще один базовый класс из библиотеки OVM.
ovm_sequencer - это пример параметризованного класса в SystemVerilog. Для тех, кто знаком с объектно-ориентированным программированием, скажу, что параметризованные классы в SystemVerilog похожи на шаблоны классов в C++. Параметризованный класс позволяет модифицировать некоторые свойства класса SystemVerilog в момент его создания. В данном случае при инстанцировании класса ovm_sequencer мы задаем вид транзакции, генерируемой этим контроллером. Таким образом, класс my_sequencer расширяет ovm_sequencer с целью генерировать транзакции типа my_transaction.
Поскольку этот класс - стандартный компонент OVM,его код должен следовать стандарту написания компонентов.Мы регистрируем компонент макросом ovm_component_utils и включаем стандартный конструктор. Больше ничего не требуется. Это очень простой контроллер и тем не менее в него встроена возможность генерировать последовательность транзакций. Далее мы увидим, как можно заставить контроллер делать то, что нам нужно в этом конкретном случае.
Мы изменяем контроллер так, чтобы он производил нужные нам действия, запуская на нем последовательность. В этом месте терминология OVM становится несколько путаной. На предыдущем слайде был показан класс ovm_sequencer с буквой r в конце. А теперь мы встречаем класс ovm_sequence. Напомним, что контроллер (sequencer) - это компонент, структурный элемент, принадлежащий иерархии компонентов. С другой стороны, последовательность (sequence) - это класс, расширяющий ovm_transaction. Последовательность состоит из данных, динамически изменяющихся во времени. Последовательность запускается на контроллере. На данном слайде показана определенная пользователем последовательность my_sequence. Она расширяет базовый класс ovm_sequence - еще один пример параметризованного класса. Он параметризуется типом транзакций, из которых составлена последовательность. Последовательность ovm_sequence всегда состоит из транзакций. То есть последовательность - это на самом деле последовательность транзакций.
Ну а далее идет стандартный код. Мы должны зарегистрировать последовательность с помощью макроса ovm_object_utils. У последовательности имеется стандартный конструктор. А теперь более интересная часть - у последовательности есть метод body.
С этим мы еще не сталкивались. Весь содержательный код, описывающий фактическое поведение последовательности, должен находиться в определяемой пользователем задаче body. В каком-то смысле метод body аналогичен фазовым методам, хотя и не принадлежит к числу стандартных фаз OVM. Метод body применяется только в последовательностях и определяет существенное поведение последовательности.
Он исполняется, когда последовательность запускается. Посмотрим, как выглядит метод body для данной конкретной последовательности. На слайде показана задача body. Как видите, она содержит бесконечный цикл. Значит, после запуска эта последовательность будет генерировать непрерывный поток транзакций, который можно остановить, только принудительно завершив моделирование, например, путем вызова метода stop_request. Ничто не может помешать вам создать последовательность, содержащую бесконечный цикл, которая, следовательно, будет генерировать непрерывный поток транзакций. Но можно вместо этого создать последовательность, которая работает конечное время, а потом останавливается. Оба решения имеют полное право на существование, нужно лишь понять, какое из них лучше отвечает имеющейся задаче верификации. В данном случае последовательность будет генерировать непрерывный поток транзакций, пока мы не прекратим моделирование.
Внутри цикла, чтобы сгенерировать очередную транзакцию в данной последовательности, необходимо выполнить несколько стандартных шагов. Первым делом мы создаем объект транзакции, для чего в очередной раз прибегаем к использованию фабричного метода. Раньше я говорил о фабричных методах в контексте создания компонентов. Сейчас же мы применяем фабричный метод create для создания экземпляра транзакции. Но идея та же самая. На первый взгляд кажется, что создается транзакция типа my_transaction, но, поскольку это фабричный метод, в тесте есть возможность переопределить тип фактически генерируемой транзакции. Следовательно, в конкретном тесте можно динамически изменить последовательность, так что она будет генерировать транзакции другого типа. Технически между этим методом и фабричным методом для генерации компонентов есть только одно различие: в данном случае методу create не передается второй аргумент, определяющий родителя, поскольку никакого родителя у транзакции не существует.
Это был первый шаг - создание транзакции. Последующие шаги со второго по четвертый составляют стандартный шаблон кода. На шаге 2 вызывается метод start_item, на шаге 3 транзакция рандомизируется, а на шаге 4 вызывается метод finish item. Итак, мы создаем транзакцию, сигнализируем инфраструктуре, что начинаем процесс ее обработки, рандомизируем объект транзакции и вызываем finsh_transaction, сообщая о том, что обработка закончена. Методы start_item и finish_item запускают внутренние механизмы взаимодействия с драйвером, который будет потребителем данной транзакции.
Итак, я показал, как создать экземпляр контроллера, еще одного стандартного компонента OVM; и как создать последовательность, которая будет выполняться на данном контроллере. Теперь пришло время взглянуть на драйвер.
Драйвер - это тоже стандартный компонент OVM. Класс пользовательского драйвера расширяет класс ovm_driver, который параметризован типом транзакции, потребляемой данным драйвером.
При написании класса мы делаем все, что необходимо для компонента OVM. Как видим, присутствует регистрация драйвера, объявление виртуального интерфейса, конструктор, методы new и build и, наконец, задача run.
Но сейчас мы сразу перейдем к рассмотрению задачи run, потому что все остальные элементы нам уже знакомы. Задача run для данного драйвера содержит цикл, то есть этот драйвер потребляет ровно четыре транзакции. В цикле драйвер синхронизируется по фронту синхроимпульса. Напомним, что именно драйвер подает и считывает сигналы с отдельных контактов тестируемого устройства. Поэтому в драйвере может присутствовать интерфейс к уровню логических элементов или уровню межрегистровых передач тестируемого устройства.
Этот конкретный драйвер взаимодействует с RTL-кодом, в самом начале он синхронизируется по фронту синхроимпульса в интерфейсе тестируемого устройства. Это делается в предложении @(posedge dut_vi.clock).
Затем, по достижении готовности, драйвер получает транзакцию от контроллера. То есть драйвер потребляет транзакции, генерируемые контроллером. Для этого драйвер вызывает метод get порта sequence_item_port. Напомню, на предыдущем занятии мы видели, что порт - это по существу описатель, который применяется для организации взаимодействия между компонентом и окружающим миром.
Получив транзакцию, драйвер может обратиться к отдельным ее полям и присвоить значения полям виртуального интерфейса устройства.
И наконец, потребив все четыре транзакции, драйвер может вызвать метод stop_request, чтобы прекратить моделирование. Таким образом, драйвер потребляет транзакции и подает сигналы на входы тестируемого устройства.
Подведем итоги. На этом занятии я показал, как определяется транзакция, как создается экземпляр контроллера, как создается последовательность, которая будет запущена на этом контроллере, как определяется и создается драйвер. А ранее мы видели, как соединить контроллер с драйвером, а драйвер с тестируемым устройством. На следующем занятии мы подробнее поговорим о контроллерах и о том, как запускать последовательности на контроллерах.