Промышленное производство
Промышленный Интернет вещей | Промышленные материалы | Техническое обслуживание и ремонт оборудования | Промышленное программирование |
home  MfgRobots >> Промышленное производство >  >> Industrial programming >> VHDL

Освоение ограниченной случайной верификации с OSVVM

Ограниченная случайная верификация (Constrained Random Verification) — стратегия тестбенча, основанная на генерации псевдослучайных транзакций для устройства под тестом (DUT). Цель — достичь функционального покрытия набора предопределенных событий через случайное взаимодействие с DUT.

Open Source VHDL Verification Methodology (OSVVM) — бесплатная VHDL-библиотека, включающая ряд удобных пакетов для создания ограниченных случайных тестбенчей. Нас особенно интересуют RandomPkg и CoveragePck, которые мы будем использовать в этой статье. Рекомендую посетить страницу OSVVM на GitHub, чтобы узнать больше о возможностях этой библиотеки.

Устройство под тестом

Я сразу перейду к примеру, чтобы лучше объяснить, чем ограниченный случайный тестбенч отличается от классического, использующего направленные тесты. Мы создали кольцевой буфер FIFO в предыдущей статье блога, но не создали самопроверяющий тестбенч для верификации корректности модуля.

Мы собираемся создать полноценный тестбенч для кольцевого буфера FIFO, использующий ограниченную случайную верификацию.

 entity ring_buffer is generic ( RAM_WIDTH : natural; RAM_DEPTH : natural ); port ( clk : in std_logic; rst : in std_logic; -- Write port wr_en : in std_logic; wr_data : in std_logic_vector(RAM_WIDTH - 1 downto 0); -- Read port rd_en : in std_logic; rd_valid : out std_logic; rd_data : out std_logic_vector(RAM_WIDTH - 1 downto 0); -- Flags empty : out std_logic; empty_next : out std_logic; full : out std_logic; full_next : out std_logic; -- The number of elements in the FIFO fill_count : out integer range RAM_DEPTH - 1 downto 0 ); end ring_buffer; 

Сущность модуля кольцевого буфера показана в коде выше. Мы будем относиться к DUT как к черному ящику, то есть не будем предполагать знаний о том, как DUT реализован. В конце концов, эта статья о тестбенче, а не о кольцевом буфере FIFO.

Мы инстанцируем DUT в тестбенче методом инстанциации сущности. Инстанциация тривиальна, поэтому я опущу код пока, но он может быть скачан позже в этой статье.

Generic-параметры DUT будут отображены на следующие значения:

Стратегия тестбенча

Пройдемся по стратегии тестирования перед началом реализации. Изображение ниже показывает основную концепцию тестбенча, который мы собираемся создать.

Освоение ограниченной случайной верификации с OSVVM

Мы будем выполнять случайные транзакции записи на входной стороне DUT. Входные данные будут устанавливаться в случайное значение на каждом тактовом цикле, а строба на входе разрешения записи будут иметь случайную длительность.

Аналогично, мы будем выполнять чтения случайно. Будем утверждать сигнал разрешения чтения в пакетах, длящиеся случайное число тактовых циклов.

Будет поведенческая модель параллельно с DUT. Это FIFO, реализованный иначе, чем кольцевой буфер в DUT, но с тем же интерфейсом. В отличие от DUT, поведенческая модель не должна быть синтезируемой. Это дает нам свободу использовать продвинутые VHDL-функции для ее создания.

Мы будем сравнивать выход DUT с выходом поведенческой модели в отдельном процессе. Этот процесс будет единственно отвечать за это сравнение на каждом тактовом цикле с помощью операторов assert. Если две FIFO-реализации поведут себя по-разному в любой момент, сбой утверждения приведет к завершению симуляции с ошибкой.

Наконец, мы соберем данные функционального покрытия, наблюдая транзакции, идущие к DUT и исходящие от него. Точка функционального покрытия может означать, например, одновременное чтение и запись, или то, что FIFO был заполнен хотя бы раз. Мы будем мониторить эти события в нашем главном процессе секвенсора тестбенча. Симуляция остановится, когда все отслеживаемые нами события функционального покрытия произойдут.

Импорт библиотеки OSVVM

Библиотека OSVVM может использоваться с любым симулятором, поддерживающим VHDL-2008. Она может уже входить в стандартные библиотеки, идущие с вашим симулятором. Она включена в ModelSim PE Student Edition, который можно скачать бесплатно от Mentor Graphics.

ModelSim поставляется со старой версией OSVVM, но это нормально, в ней есть все, что нам нужно. Мы можем просто импортировать пакеты random и coverage так:

 library osvvm; use osvvm.RandomPkg.all; use osvvm.CoveragePkg.all; 

Последняя версия библиотеки OSVVM всегда доступна для скачивания со страницы GitHub. Сделайте это, если ваш симулятор не включает её, или если вы хотите использовать новейшие возможности библиотеки.

Объявление переменных OSVVM

Библиотека OSVVM содержит пакеты с защищенными типами. Переменные, созданные этих типов, были бы ограничены областью видимости процесса, в котором они определены. Поэтому мы вместо этого объявим их как общие переменные (shared variables) в декларативной области архитектуры тестбенча, как показано в коде ниже.

 -- OSVVM variables shared variable rv : RandomPType; shared variable bin1, bin2, bin3, bin4, bin5, bin6 : CovPType; 

Переменная rv типа RandomPType используется для генерации случайных значений. Нам нужен только один такой объект, потому что мы можем использовать один и тот же объект в каждом процессе, где нужна генерация случайных значений. Последняя строка кода объявляет шесть переменных типа CovPType.

Мы объявили шесть переменных покрытия, потому что у нас будет шесть целей покрытия, мы будем называть эти объекты «бинами» (bins). Общие переменные должны быть инициализированы перед использованием для сбора данных покрытия. Мы делаем это, вызывая процедуру AddBins на каждом из бинов CovPType.

 -- Set up coverage bins bin1.AddBins("Write while empty", ONE_BIN); bin2.AddBins("Read while full", ONE_BIN); bin3.AddBins("Read and write while almost empty", ONE_BIN); bin4.AddBins("Read and write while almost full", ONE_BIN); bin5.AddBins("Read without write when almost empty", ONE_BIN); bin6.AddBins("Write without read when almost full", ONE_BIN); 

Мы передаем строковое описание бина покрытия как первый параметр процедуре AddBins. Эта строка вернется в конце симуляции, когда мы распечатаем статистику для каждого бина покрытия. Как видите из текстовых описаний, мы собираемся использовать бины для проверки того, произошли ли некоторые очень специфические граничные случаи.

AddBins — перегруженная процедура, которая может использоваться для создания нескольких счетчиков внутри переменных бинов. Однако у нас будет только один счетчик, ассоциированный с каждым бином. Поэтому мы передадим константу удобства ONE_BIN как параметр процедуре AddBins. Это инициализирует переменные CovPType с одним бином каждый. Счетчики, представленные бинами, считаются покрытыми, когда события, за которыми они следят, произошли хотя бы раз.

Генерация случайного входа

Начнем с создания процесса, генерирующего входные данные для DUT. Кольцевой буфер FIFO спроектирован так, чтобы игнорировать попытки перезаписи и перечтения. Поэтому мы можем просто писать случайные данные пакетами случайной длительности. Нам не нужно думать, готов ли DUT фактически поглотить данные или нет.

 PROC_WRITE : process begin wr_en <= rv.RandSlv(1)(1) and not rst; for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop wr_data <= rv.RandSlv(RAM_WIDTH); wait until rising_edge(clk); end loop; end process; 

Единственное соображение этого процесса — что DUT не в сбросе. Мы случайно включаем или отключаем сигнал разрешения записи в первой строке этого процесса, но он будет включен только если rst равен '0'.

Последующий for-loop будет писать случайные данные в DUT на случайное число тактовых циклов, даже если сигнал разрешения не активен. Мы можем это делать, потому что DUT должен игнорировать порт wr_data, если сигнал wr_en не '1'. После for-loop программа вернется к началу процесса, запуская другую случайную транзакцию записи.

Выполнение случайных чтений

Процесс, читающий данные из DUT, похож на процесс записи. Мы можем случайно активировать сигнал rd_en в любое время, потому что DUT спроектирован так, чтобы игнорировать попытки чтения при пустоте. Данные, появляющиеся на порту rd_data, не проверяются на самом деле. Этот процесс только управляет сигналом разрешения чтения.

 PROC_READ : process begin rd_en <= rv.RandSlv(1)(1) and not rst; for i in 0 to rv.RandInt(0, 2 * RAM_DEPTH) loop wait until rising_edge(clk); end loop; end process; 

Поведенческая верификация

Мы построим поведенческую модель DUT внутри нашего тестбенча для верификации его поведения. Это известная стратегия тестбенча. Сначала мы кормим поведенческую модель одновременно с теми же входами, что и DUT. Затем мы можем сравнить выходы от обоих, чтобы проверить, имеет ли DUT правильное поведение.

Освоение ограниченной случайной верификации с OSVVM

Изображение выше показывает базовую компоновку такого тестбенча. Поведенческая модель работает параллельно с DUT. Мы используем её как эталон для проверки выходов DUT.

Тестбенч FIFO

Мы будем использовать связанный список для реализации поведенческой модели. Связанные списки нельзя синтезировать, но они идеальны для тестбенчей. Вы можете вспомнить статью Как создать связанный список в VHDL, если вы регулярный читатель этого блога. Мы используем код из неё для реализации поведенческой модели для кольцевого буфера FIFO.

 package DataStructures is type LinkedList is protected procedure Push(constant Data : in integer); impure function Pop return integer; impure function IsEmpty return boolean; end protected; end package DataStructures; 

Объявление пакета для связанного списка FIFO показано в коде выше. Это защищенный тип с тремя функциями: Push, Pop и IsEmpty. Они используются для добавления и удаления элементов из FIFO, а также для проверки, осталось ли ноль элементов.

 -- Testbench FIFO that emulates the DUT shared variable fifo : LinkedList; 

Защищенные типы — классоподобные конструкции в VHDL. Мы создадим объект связанного списка, объявив общую переменную в декларативной области тестбенча, как показано в коде выше.

Поведенческая модель

Для полной эмуляции поведения кольцевого буфера FIFO, мы объявляем два новых сигнала, зеркалящих выходные сигналы DUT. Первый сигнал содержит выходные данные от поведенческой модели, второй — ассоциированный сигнал valid.

 -- Testbench FIFO signals signal fifo_out : integer; signal fifo_out_valid : std_logic := '0'; 

Код выше показывает объявление двух выходных сигналов от поведенческой модели. Нам не нужно никаких выделенных входных сигналов для поведенческой модели, потому что они те же, что подключены к DUT. Мы используем сигналы для эмуляции выхода DUT, потому что это позволяет легко собирать данные покрытия, как мы увидим позже в этой статье.

 PROC_BEHAVIORAL_MODEL : process begin wait until rising_edge(clk) and rst = '0'; -- Emulate a write if wr_en = '1' and full = '0' then fifo.Push(to_integer(unsigned(wr_data))); report "Push " & integer'image(to_integer(unsigned(wr_data))); end if; -- Emulate a read if rd_en = '1' and empty = '0' then fifo_out <= fifo.Pop; fifo_out_valid <= '1'; else fifo_out_valid <= '0'; end if; end process; 

Процесс, реализующий поведенческую модель кольцевого буфера FIFO, показан в коде выше. Этот процесс будет запускаться на каждом нарастающем фронте тактового сигнала, если сигнал сброса не активен.

Поведенческая модель помещает новое значение в тестбенч FIFO всякий раз, когда сигнал wr_en утвержден, пока сигнал full равен '0'. Аналогично, логика чтения в процессе поведенческой модели работает, слушая сигналы rd_en и empty. Последний контролируется DUT, но мы проверим, что он работает в следующем процессе, который создадим.

Проверка выходов

Вся логика, отвечающая за проверку выходов DUT, собрана в процессе под названием «PROC_VERIFY». Мы используем операторы assert для проверки того, что выходы DUT совпадают с выходами от поведенческой модели. Мы также проверяем, что DUT и поведенческая модель согласны в том, когда FIFO пуст.

Код для процесса верификации показан ниже.

 PROC_VERIFY : process begin wait until rising_edge(clk) and rst = '0'; -- Check that DUT and TB FIFO are reporting empty simultaneously assert (empty = '1' and fifo.IsEmpty) or (empty = '0' and not fifo.IsEmpty) report "empty=" & std_logic'image(empty) & " while fifo.IsEmpty=" & boolean'image(fifo.IsEmpty) severity failure; -- Check that the valid signals are matching assert rd_valid = fifo_out_valid report "rd_valid=" & std_logic'image(rd_valid) & " while fifo_out_valid=" & std_logic'image(fifo_out_valid) severity failure; -- Check that the output from the DUT matches the TB FIFO if rd_valid then assert fifo_out = to_integer(unsigned(rd_data)) report "rd_data=" & integer'image(to_integer(unsigned(rd_data))) & " while fifo_out=" & integer'image(fifo_out) severity failure; report "Pop " & integer'image(fifo_out); end if; end process; 

Процесс запускается нарастающим фронтом тактового сигнала, как видно из первой строки кода. DUT — тактированный процесс, и нижестоящая логика, подключенная к DUT, также ожидается синхронной с тактовым сигналом. Поэтому имеет смысл проверять выходы на нарастающем фронте такта.

Второй блок кода проверяет, что сигнал empty от DUT утверждается только когда тестбенч FIFO пуст. DUT и поведенческая модель оба должны соглашаться, что FIFO пуст или нет, для прохождения этого теста.

Затем идет сравнение сигналов valid данных чтения. DUT и поведенческая модель должны выводить данные одновременно, иначе что-то не так.

Наконец, мы проверяем, что выходные данные от DUT совпадают со следующим элементом, который мы извлекаем из тестбенч FIFO. Это, конечно, происходит только если сигнал rd_valid утвержден, означая, что сигнал rd_data может быть засемплирован.

Сбор данных покрытия

Для управления основным потоком тестбенча мы создадим процесс секвенсора. Этот процесс инициализирует бины покрытия, запускает тесты и останавливает тестбенч, когда все цели покрытия достигнуты. Код ниже показывает полный процесс «PROC_SEQUENCER».

 PROC_SEQUENCER : process begin -- Set up coverage bins bin1.AddBins("Write while empty", ONE_BIN); bin2.AddBins("Read while full", ONE_BIN); bin3.AddBins("Read and write while almost empty", ONE_BIN); bin4.AddBins("Read and write while almost full", ONE_BIN); bin5.AddBins("Read without write when almost empty", ONE_BIN); bin6.AddBins("Write without read when almost full", ONE_BIN); wait until rising_edge(clk); wait until rising_edge(clk); rst <= '0'; wait until rising_edge(clk); loop wait until rising_edge(clk); -- Collect coverage data bin1.ICover(to_integer(wr_en = '1' and empty = '1')); bin2.ICover(to_integer(rd_en = '1' and full = '1')); bin3.ICover(to_integer(rd_en = '1' and wr_en = '1' and empty = '0' and empty_next = '1')); bin4.ICover(to_integer(rd_en = '1' and wr_en = '1' and full = '0' and full_next = '1')); bin5.ICover(to_integer(rd_en = '1' and wr_en = '0' and empty = '0' and empty_next = '1')); bin6.ICover(to_integer(rd_en = '0' and wr_en = '1' and full = '0' and full_next = '1')); -- Stop the test when all coverage goals have been met exit when bin1.IsCovered and bin2.IsCovered and bin3.IsCovered and bin4.IsCovered and bin5.IsCovered and bin6.IsCovered; end loop; report("Coverage goals met"); -- Make sure that the DUT is empty before terminating the test wr_en <= force '0'; rd_en <= force '1'; loop wait until rising_edge(clk); exit when empty = '1'; end loop; -- Print coverage data bin1.WriteBin; bin2.WriteBin; bin3.WriteBin; bin4.WriteBin; bin5.WriteBin; bin6.WriteBin; finish; end process; 

Сначала мы инициализируем объекты бинов покрытия, вызывая процедуру AddBins на них, как мы уже обсуждали ранее в этой статье. Затем, после снятия сброса, мы переходим к сбору данных покрытия. На каждом нарастающем фронте тактового сигнала код внутри конструкции loop будет выполняться.

Первый блок кода внутри цикла — для сбора данных покрытия. Мы можем вызвать процедуру ICover на бине, чтобы зафиксировать попадание в точку покрытия, которую он представляет. Если мы передаем целочисленный параметр 0, вызов не будет иметь эффекта. Если мы используем целочисленный параметр 1, это засчитается как попадание.

Внутри каждого объекта бина покрытия только один «бин», потому что мы инициализировали их с константой ONE_BIN. Этот единственный бин может быть достигнут вызовом ICover(1). Мы можем зарегистрировать попадание или промах в точке покрытия, конвертируя наши булевы выражения в целые 1 или 0 с помощью функции to_integer.

После записи данных покрытия мы проверяем, достигнуты ли все цели покрытия, вызывая функцию IsCovered на всех бинах. Затем выходим из цикла, если все цели покрытия достигнуты.

Мы убедимся, что DUT пуст перед завершением теста. Для этого мы забираем управление у процессов писателя и читателя, форсируя сигнал wr_en в '0' и сигнал rd_en в '1'.

Наконец, мы печатаем статистику о том, сколько раз каждая цель покрытия была достигнута, вызывая функцию WriteBin на каждом из бинов покрытия. Ключевое слово finish в конце процесса заставит симулятор остановить симуляцию.

Запуск тестбенча

Вы можете скачать весь проект ModelSim со всеми VHDL-файлами, используя форму ниже.

После загрузки проекта выполнением включенного в ZIP do-файла, мы можем запустить тестбенч, просто введя «runtb» в консоли ModelSim. Время выполнения тестбенча будет случайным, потому что цели покрытия достигаются случайно. Однако результаты теста воспроизводимы, потому что используется псевдослучайная последовательность.

Мы не инициализировали сид в нашем коде, поэтому будет использовано значение сида по умолчанию для псевдослучайного генератора. Можно задать другой сид, вызвав процедуру InitSeed на объекте RandomPType, это даст другую случайную последовательность.

Вывод консоли

Вывод, напечатанный в консоли ModelSim после команды «runtb», показан ниже. Во время симуляции будет много случайных помещений в и извлечений из тестбенч FIFO.

VSIM 2> runtb # ** Warning: Design size of 15929 statements or 2 leaf instances exceeds # ModelSim PE Student Edition recommended capacity. # Expect performance to be quite adversely affected. # ** Note: Push 34910 # Time: 790 ns Iteration: 0 Instance: /ring_buffer_tb ... # ** Note: Pop 37937 # Time: 83100 ns Iteration: 0 Instance: /ring_buffer_tb # ** Note: Pop 13898 # Time: 83110 ns Iteration: 0 Instance: /ring_buffer_tb # %% WriteBin: # %% Write while empty Bin:(1) Count = 2 AtLeast = 1 # # %% WriteBin: # %% Read while full Bin:(1) Count = 3 AtLeast = 1 # # %% WriteBin: # %% Read and write while almost empty Bin:(1) Count = 106 AtLeast = 1 # # %% WriteBin: # %% Read and write while almost full Bin:(1) Count = 1 AtLeast = 1 # # %% WriteBin: # %% Read without write when almost empty Bin:(1) Count = 1 AtLeast = 1 # # %% WriteBin: # %% Write without read when almost full Bin:(1) Count = 3 AtLeast = 1 # # Break in Process PROC_SEQUENCER at C:/crv/ring_buffer_tb.vhd line 127 

Статистика для всех бинов покрытия печатается, когда все цели покрытия достигнуты. Некоторые бины достигнуты только один раз, а один — 106 раз. Но в конце каждый бин был достигнут хотя бы раз. Таким образом, мы можем знать, что все события, для которых мы определили бины покрытия, были протестированы и верифицированы.

Волновая форма

Изучим волновую форму, чтобы получить представление о том, что делал тестбенч. Изображение ниже показывает волновую форму с сигналом fill_count, представленным как аналоговое значение. FIFO полон, когда трассировка этого сигнала на вершине, и пуст, когда она внизу.

Освоение ограниченной случайной верификации с OSVVM

Как видно из волновой формы, кольцевой буфер заполняется и опорожняется случайно. Тем не менее, мы не программировали явно эти нарастания и спады уровня заполнения. Тем не менее, мы видим органически выглядящий паттерн взаимодействия с DUT.

Больше об ограниченной случайной верификации

Ограниченная случайная верификация — хорошая стратегия тестбенча, когда тестовый вектор имеет слишком много перестановок для того, чтобы исчерпывающий тест был практичным. Случайные взаимодействия демонстрируют более естественное поведение, чем направленный угловой тест, не жертвуя точностью.

Мы можем быть уверены, что все угловые случаи достигнуты, пока мы правильно настроили сбор данных покрытия. Дополнительная выгода в том, что рандомизированное тестирование с большей вероятностью раскроет слабости в DUT, которые вы не тестируете специально. Пока вы знаете все угловые случаи, вы можете создавать направленные тесты для них. Но угловые случаи легко упускаются, и именно тогда вы можете выиграть от методологии ограниченной случайной верификации.

Эта статья лишь приоткрыла поверхность того, что можно сделать с ограниченной случайной верификацией. Рекомендую читать документацию на странице OSVVM на GitHub, чтобы копнуть глубже в тему.

Я также рекомендую курс SynthWorks Advanced VHDL Testbenches and Verification, с которым я не аффилирован. Однако я посещал 5-дневную версию этого очного курса. Курс ведет جيم Льюис, председатель группы анализа и стандартизации VHDL (VASG). В целом, отличная инвестиция для любой компании, желающей поднять свои VHDL-тестбенчи на следующий уровень.

VHDL

  1. Cadence представляет решения эмуляции и прототипирования Rapid-Performance для верификации SoC с миллиардами гейтов
  2. Siemens расширяет систему Veloce четырьмя новыми продуктами, обеспечивая бесшовную аппаратно-ускоренную верификац…
  3. Synopsys представляет полный набор HBM3 IP для много-кристаллических систем, повышая пропускную способность до 921 ГБ…
  4. Интегрированная система контроля доступа с QR, RFID и верификацией температуры
  5. Непредсказуемые вызовы обслуживания: Инсайты эксперта Джейсона Афары
  6. Генерация случайных чисел в Java: Практическое руководство с Random и Math.random
  7. Java 8 Streams: декларативная обработка данных
  8. Hurco TM8: компактный косогорелый токарный станок с точным управлением и универсальным оснащением
  9. Проектирование высокоскоростных печатных плат: обработка дифференциальных сигналов и симуляционная верифи…
  10. Программа верификации производительности роторных компрессоров CAGI: гарантия надежной и эффективной работы