Разработка и тестирование целочисленного сумматора с AXI-Stream интерфейсами
Часть 3
Автор: https://github.com/VSHEV92
Оглавление
Введение
Direct Testing
Constraint Random Testing
Структура тестового окружения
Компоненты тестового окружения
Тестовое окружение для проверки сумматора
Заключение
Введение
В предыдущей статье мы познакомились с основами работы AXI-Stream протокола и модифицировали наш сумматор, чтобы он был совместим с этим интерфейсом. Также было отмечено, что из-за увеличения сложности сумматора встает проблема в его тестировании. Напрямую генерировать все возможные входные воздействия достаточно сложно из-за большого количества их различных вариантов. Еще утомительней каждый раз вручную просматривать временные диаграммы в поисках ошибок. Нам нужен другой подход, и именно это мы будем обсуждать в этой статье.
Direct Testing
Для начала рассмотрим метод, который чаще всего применяется начинающими разработчиками для проверки своих модулей. В качестве примера возьмем простой комбинационный сумматор. Напишем тестовое окружение, которое будет подавать на входы сумматора несколько пар слагаемых, имеющих наперед заданные значения. Такое окружение, например, может иметь следующий вид:
// Testbench для проверки комбинационного сумматора
module adder_comb_tb ();
localparam integer WIDTH = 4; // разрядность входных данных
// входные и выходные сигналы
reg [WIDTH-1:0] data1_i;
reg [WIDTH-1:0] data2_i;
wire [WIDTH:0] data_o;
// проверяемый модуль
adder_comb #(
.WIDTH(WIDTH)
) dut (
.data1_i(data1_i),
.data2_i(data2_i),
.data_o (data_o)
);
// Формирование входных воздействий
initial begin
data1_i = 1;
data2_i = 3;
#10;
data1_i = 13;
data2_i = 6;
#10;
data1_i = 9;
data2_i = 7;
#10;
data1_i = 4;
data2_i = 10;
#10;
$finish;
end
endmodule
На сумматор подаются четыре пары слагаемых. Значения слагаемых задаются в тесте явным образом. Сначала на входы сумматора поступают числа 1 и 3. Соответственно мы ожидаем, что сумматор выдаст результат равный 4. Через 10ns мы подаем следующие значения, равные 13 и 6, и так далее.
Такой подход, при котором мы самостоятельно формируем конкретные входные воздействия называется direct testing. Именно он был использован при создании тестового окружения для сумматора с входными и выходными сигналами валидности в самой первой статье. Это вполне применимо для тестирования небольших модулей, но когда сложность устройства увеличивается, его проверка с помощью direct testing становится затруднительной.
Давайте выясним причину. Наше цифровое устройство, как правило, является последовательным и имеет в своем составе множество триггеров. В каждый момент времени каждый триггер хранит определенное значение. Совокупность этих значений определяет текущее состояние устройства. Будем называть множество всех возможных состояний, в которых может находиться наше устройство, пространством состояний.
Постараемся представить себе это графически. Пускай у нас есть некоторая область, точки которой обозначают возможные и невозможные состояния нашего модуля. На рисунке ниже в качестве примера эта область изображена в виде параллелепипеда. Все возможные состояния образуют пространство состояний устройства. Эти состояния отмечены зелеными и красными точками. Зелеными точками обозначены состояния, в которых устройство работает корректно, а красными — состояния с ошибками. В процессе тестирования мы должны найти все красные точки, и сделать так, чтобы они стали зелеными.
Если для проверки используется direct testing, то мы самостоятельно формируем входные воздействия, в результате которых наше устройство будет проходить через последовательность некоторых состояний. Символически на рисунке это можно представить в виде ломанной линий, которая соединяет точки из пространства состояний. Тогда, если будет сформировано десять разных вариантов входных воздействий, то мы получим десять путей в пространстве состояний.
В этой терминологии задача тестирования на основе direct testing заключается в формировании набора входных воздействий таким образом, чтобы через каждое состояние проходил хотя бы один путь. Но в сложных цифровых устройствах состояний может быть так много, что чисто физически невозможно сформировать все необходимые пути. Более того, могут быть такие состояния, о существовании которых мы даже не догадываемся. При этом они также могут содержать ошибки и приводить к некорректной работе устройства. Если мы не знаем, что такие состояния возможны, то при direct testing мы не сформируем нужные входные воздействия и не проверим их.
Constraint Random Testing
Из-за рассмотренных выше сложностей при ручном формировании входных воздействий, для проверки сложных цифровых устройств сейчас применяется другой поход, который называется constraint random testing. Его идея заключается в том, что мы не создаем входные воздействия в явном виде, а генерируем их случайным образом. Например, тестовое окружение сумматора, построенное по такому принципу, показано ниже:
// Testbench для проверки комбинационного сумматора
module adder_comb_tb ();
localparam integer WIDTH = 4; // разрядность входных данных
integer seed = 0; // начальное значение генератора случайных чисел
integer trans_number = 10; // число суммирований
// входные и выходные сигналы
reg [WIDTH-1:0] data1_i;
reg [WIDTH-1:0] data2_i;
wire [WIDTH:0] data_o;
// проверяемый модуль
adder_comb #(
.WIDTH(WIDTH)
) dut (
.data1_i(data1_i),
.data2_i(data2_i),
.data_o (data_o)
);
// Формирование входных воздействий. В цикле формируем случайные значения
// входных слагаемых.
initial begin
repeat (trans_number) begin
data1_i = $urandom(seed);
data2_i = $urandom(seed);
#10;
end
$finish;
end
endmodule
В данном окружении мы с помощью цикла repeat
формируем заданное число (trans_number
) случайных пар входных слагаемых. Функция $urandom()
возвращает беззнаковое целое число и выступает в роли генератора псевдослучайных чисел. С помощью переменной seed
мы можем устанавливать начальное состояние этого генератора. Изменяя значение seed
, мы будем получать разные последовательности случайных чисел.
Однако, как правило, при взаимодействии с устройством предполагается некоторый протокол. Поэтому мы не можем генерировать входные воздействия абсолютно случайным образом. Случайные входные сигналы необходимо формировать с учетом определенных ограничений (constrainst), чтобы не нарушались правила взаимодействия с тестируемым устройством.
Как и для direct testing, постараемся представит, как данная концепция будет выглядеть в терминах пространства состояний. При constraint random testing пути в пространстве состояний становятся случайными. При одном запуске теста путь может пройти по одной последовательности состояний, при следующем запуске — по другой. Таким образом, мы можем запускать один и тот же тест множество раз, при этом каждый раз исследуя новые области пространства состояний.
Накладывая на генератор случайных входных воздействий дополнительные ограничения, мы будем получать семейства путей, которые будут проходить по разным частям пространства состояний. Более того, некоторые пути могут проходить через состояния, о которых мы не догадывались. Таким образом, мы можем обнаружить ошибки, которые остались бы незамеченными при direct testing.
Однако, такой подход имеет и свои сложности. Иногда цифровое устройство может иметь состояния, в которые очень сложно попасть, блуждая по всему пространству случайным образом. Эти состояния обычно описывают очень специфические, краевые условия работы (corner cases). Чтобы до них добраться с помощью constraint random testing, может потребоваться множество запусков случайных тестов со сложными ограничениями на входные воздействия. В этом случае такие состояния обычно проверяются с помощью direct testing.
Таким образом, можно предложить следующие рекомендации при тестировании сложных модулей:
Структура тестового окружения
Мы разобрались, что сложные модули лучше тестировать, используя случайные входные воздействия. Но теперь встает вопрос:, а как мы будем проверять результаты этих воздействий? Так как при direct testing разработчик вручную формирует входные сигналы, то он представляет, что он должен увидеть на временных диаграммах после запуска теста. Однако, теперь входные воздействия создаются случайным образом с помощью генератора. Поэтому, если мы собираемся просматривать временные диаграммы глазами, то сначала нам требуется понять, что было подано на вход, а потом определить правильный ли получился результат на выходе модуля. Это очень трудоемко и утомительно, поэтому нужно автоматизировать этот процесс.
Для этого нам необходимо добавить в тестовое окружение эталонную модель (gold model), с которой можно сравнивать выходные сигналы тестируемого устройства. Эта модель не должна синтезироваться, поэтому ее можно описывать на более высоком уровне абстракции. Достаточно часто, модели пишутся на других языках программирования, например на C или Python, и подключаются к симулятору с помощью специальных программных интерфейсов (API). Для VHDL такой интерфейс называется VHPI, для Verilog — VPI, для SystemVerilog — DPI. Тема программных интерфейсов весьма обширна, поэтому мы не будем на ней останавливаться. Интересующимся читателям посоветуем заглянуть в стандарт соответствующего языка.
Тестовое окружение построенное по такой схеме называют self-test testbench. В процессе проверки окружение генерирует случайные входные воздействия и подает их одновременно на тестируемое устройство и эталонную модель. Затем их отклики сравниваются, и в случае обнаружения несоответствия выводится сообщение об ошибке. Также обычно после завершения теста в консоль симулятора выводится небольшой отчет, который может включать в себя настройки окружения, количество ошибок, длительность моделирования и т.д. Таким образом, после завершения симуляции нам требуется только заглянуть в отчет, чтобы понять прошел тест успешно или нет.
Наше тестовое окружение становится более продвинутым, и также как мы действовали ранее при усложнении сумматора, нам нужно постараться разбить его на отдельные более простые части. Как правило, выделяют следующие компоненты окружения: generator, driver, monitor, scoreboard. Типовая структура тестового окружения представлена ниже:
Давайте рассмотрим назначение каждого компонента по отдельности.
Компоненты тестового окружения
Генератор (Generator) — задача генератора заключается в создании входных данных и некоторых служебных параметров. Например, если мы проверяем модуль памяти, то генератор должен сформировать адрес, тип запроса (запись или чтение), и если осуществляется запись, то также записываемые данные. Обратим внимание, что генератор создает именно информационную составляющую входного воздействия, которую также называют транзакцией. Генератор не отвечает за то, с какой задержкой будет передана эта транзакция и как будет осуществляться handshake. Это задача следующего компонента.
Драйвер (Driver) — драйвер должен получать транзакции от генератора и передавать их проверяемому устройству согласно заданному протоколу. Например, для нашего сумматора входная транзакция состоит только из значения слагаемого. Задача драйвера передать его сумматору согласно правилам AXI-Stream интерфейса. То есть, драйвер отвечает за то, как правильно и с какими случайными задержками будут сформированы сигналы tvalid
или tready
.
Монитор (Monitor) — монитор следит за активностью на внешних интерфейсах проверяемого модуля и, зная протокол взаимодействия, пытается обнаружить и восстановить переданные транзакции. Для AXI-Stream интерфейса монитор должен следить за сигналами tvalid
и tready
и в момент, когда происходит handshake, защелкивать данные на шине tdata
.
Scoreboard — этот компонент оценивает корректность работы тестируемого модуля. Он может включать в себя несколько более простых блоков. Достаточно часто он содержит эталонную модель и checker — компонент выполняющий сравнение результатов. В scoreboard попадают транзакции от всех мониторов. Транзакции, которые для тестируемого устройства были входными отправляются в модель, чтобы получить эталонный результат. Выходные транзакции вместе с эталонами от модели отправляются в checker, который выполняет сравнение и сообщает о наличии ошибок.
Иногда в тестовом окружении выделяют еще один компонент, который называется сторожевой таймер (watchdog). Так как входные воздействия, а также задержки при их передаче через интерфейсы модуля, теперь случайные, мы заранее не знаем за сколько времени пройдет моделирование. Обычно тест завершается автоматически, после того как в проверяемое устройство было передано заданное число транзакций. Но из-за ошибок в устройстве или окружении тест может зависнуть и нужное число транзакций никогда не будет передано. Чтобы тест не выполнялся бесконечно долго, в окружение добавляют watchdog, который завершает его спустя заданное время.
Тестовое окружение для проверки сумматора
Описанный выше подход к построению тестового окружения не является каким-либо постулатом, и в зависимости от задачи структура окружения может меняться. Рассмотренное окружение удобно описывать на языке SystemVerilog, который предоставляет возможности объектно-ориентированного программирования, динамические структуры данных и много другое. В данном цикле статей мы планируем ограничиться только конструкциями языка Verilog. Соответственно структура тестового окружения для нашего сумматора также будет немного изменена. Схема окружения для проверки сумматора с AXI-Stream интерфейсами представлена ниже:
Во-первых, для простоты объединим функционал генератора и драйвера в один компонент, который для удобства также будем называть драйвером. То есть, в нашем окружении драйвер будет отвечать за генерацию транзакций и передачу их в тестируемый модуль. Сумматор имеет два входных AXI-Stream интерфейса и один выходной. Поэтому в окружении будет присутствовать три драйвера, два из которых будут выступать в роли передатчиков, а один — как приемник. Каждому интерфейсу будет соответствовать свой монитор, восстанавливающий транзакции и передающий их в scoreboard.
Во-вторых, в scoreboard выделим еще два компонента, которые назовем коллекторы (collectors). Чтобы выполнить суммирование, мы должны получить слагаемые от двух разных источников. Они могут поступать на входные интерфейсы сумматора неодновременно, поэтому нам необходимо где-то хранить одно из слагаемых, пока не получено второе. Эту задачу как раз и будут выполнять коллекторы. Каждый коллектор будет получать транзакции от своего монитора и сохранять их в отдельный массив.
Рассмотрим как будет выполняться оценка корректности работы сумматора. Проверка начинается, когда монитор для выходного интерфейса обнаруживает транзакцию. Это говорит о том, что сумматор выполнил сложение, а значит ранее на его входные интерфейсы были поданы слагаемые, которые коллекторы уже успели сложить в свои массивы. Scoreboard может получить слагаемые из массивов, передать их в модель и получить эталонный результат. Далее checker сравнивает этот эталон с данными от тестируемого устройства и определяет, правильно ли было выполнено сложение.
Чтобы иметь возможность завершить тест в случае зависания устройства или одного из компонентов окружения, мы будем использовать сторожевой таймер.
Заключение
Данная статья по сравнению с предыдущими содержит много теоретического материала. Мы разобрались с тем, что такое direct testing и узнали об ограничениях данного подхода. Далее мы рассмотрели один из самых популярных на текущий момент методов проверки сложных цифровых устройств — constraint random testing. Он позволяет исследовать пространство состояний тестируемого устройства за счет формирования случайных входных воздействий.
Мы также узнали, как автоматизировать проверку работы устройства с помощью сравнения его выходных данных с эталонной моделью. Тестовое окружение работающее по такому принципу называется self-test testbench. Очевидно, что если проверяемое устройство будет сложным, то и тестовое окружение не получится сделать простым. Поэтому для удобства реализации его принято разбивать на отдельные компоненты, такие как генераторы, драйверы и мониторы.
В завершение мы адаптировали структуру тестового окружения для проверки нашего сумматора с AXI-Stream интерфейсами. В следующей статье мы перейдем от теории к практике и пошагово покажем, как реализовать это окружение на языке Verilog.