Введение
В этом разделе мы рассмотрим пример создания тестового окружения на SystemVerilog для решения задачи верификации, которая нередко встает перед разработчиками. Это задача проверки RTL-описания, в которое были внесены изменения, не изменяющие логику работы схемы. В основном, подобные изменения, направлены на улучшение результатов синтеза. Несколько примеров:
- Оптимизация критических цепей. Языки описания аппаратуры дают возможность описать одну и ту же схему разными способами. Даже опытный разработчик не всегда может сходу написать код, который при синтезе даст самую быструю схему. При увеличении тактовой частоты от проекта к проекту из RTL-описания “выжимается” все до последней наносекунды. Здесь приходится перебирать различные варианты, использовать специальные оптимизированные по времени IP-блоки (например, Design Ware от Synopsys).
- Перевод структурного или потокового описания в поведенческое. RTL-описание бывает трех типов: структурное, потоковое и поведенческое. Описание 1-битного сумматора для каждого типа на языке Verilog выглядит так:
а)Структурное:
б) Потоковое:- and g0(carry,in1,in2);
- xor g1(sum,in1,in2);
в) Поведенческое:- assign sum = in1 ^ in2;
- assign carry = in1 & in2;
Раньше структурный и потоковый типы описаний давали при синтезе лучший результат, нежели поведенческое, но с развитием САПР ситуация изменилась. Теперь за счет специальных библиотек или специализированных блоков, встроенных в ПЛИС, оптимизированный по времени результат при синтезе дает только поведенческое описание. Даже если цепь некритическая, перевод в поведенческое описание уменьшает размер кода и делает его нагляднее.- assign {carry, sum} = in1 + in2;
- Изменение разбиения RTL-описания на модули. Проект может быть разбит на множество модулей. Взгляд на то, как проект должен быть разбит на модули, может меняться (например, при переводе в поведенческое описание надобность в некоторых модулях пропадает). Рекомендуется, чтобы количество вентилей в модуле находилось в пределах от 5.000 до 50.000. Слишком мелкое разбиение приводит к неэффективному синтезу, слишком большое – значительно увеличивает время синтеза.
- Перемещение комбинационной логики между регистрами. Баланс логики между регистрами (register-logic balancing) – это метод, применяемый для того, чтобы сбалансировать количество комбинационной логики между регистрами для достижения максимальной производительности. В основном применяется в схемах с конвейерной архитектурой. Многие средства синтеза (например, Design Compiler) имеют встроенные команды, позволяющие на этапе синтеза перемещать логику между регистрами, но в основном комбинационную логику стараются сбалансировать на уровне RTL-описания.
На рис. 1 показана схема, которая с увеличением тактовой частоты с 500 МГц до 1ГГц, перестала удовлетворять временным ограничениям. Появилась отрицательная задержка в 0.3нс.
Рис. 1. Критический путь с отрицательной задержкой в 0.3 нс
Решить данную проблему можно переместив часть комбинационной логики с одного уровня регистров на другой (см. рис. 2):Рис. 2. Критический путь после перемещения части комбинационной логики
Теперь схема удовлетворяет наложенным на нее временным ограничениям. - Перевод описания из VHDL в Verilog и наоборот. По разным причинам иногда приходится переписывать VHDL-описание в Verilog и наоборот. Это может делаться вручную или с помощью специальных трансляторов.
Как правило, вышеперечисленные изменения вносятся в RTL-описание, которое уже показало свою состоятельность в предыдущих проектах. Новый вариант схемы должен вести себя так же, как и старый, т.е. при подаче входного воздействия обе схемы должны на выходе выдать идентичные комбинации битов. Убедиться в том, что при внесении изменений поведение схемы не изменилось, можно только перебрав все входные наборы и сравнив состояние выходов старой и новой схемы. Делать это средствами языков Verilog и VHDL – очень трудоемкое занятие. Особенно, если в схемах большое число входных и выходных портов. Надеяться на уже написанные тесты, если только они не перебирают все входные наборы, тоже нельзя. Именно поэтому подобные изменения вносятся только в случае крайней необходимости.
Для того, чтобы сократить затраты на верификацию RTL-описания при оптимизации, не изменяющей логику работы схемы, мы создадим тестовое окружение на языке SystemVerilog. За счет встроенных в язык средств объектно-ориентированного программирования (ООП) такое тестовое окружение будет обладать возможностью многократного использования, а мощный механизм генерации случайных последовательностей и средства оценки функционального покрытия, которыми также обладает SystemVerilog, позволят значительно повысить качество отладки. В результате получится IP-ядро, с помощью которого можно будет проверять идентичность описаний на языках Verilog/SystemVerilog, VHDL и SystemC.
В качестве тестируемой схемы рассмотрим простенькую схему вычисления адреса операнда:
Рис. 3. Схема вычисления адресов операндов
Verilog-описание этой схемы в потоковом стиле выглядит вот так:
- module old_scheme(
- input wire [3:0] a,
- input wire [3:0] b,
- input wire [3:0] c,
- input wire [3:0] d,
- output wire [3:0] result
- );
- wire [3:0] p1, g1, p2, g2, p3, g3;
- wire [3:0] sum1, sum2, sum3;
- wire co;
- //--> adder1
- assign p1[0]=a[0]^b[0]^c[0];
- assign p1[1]=a[1]^b[1]^c[1];
- assign p1[2]=a[2]^b[2]^c[2];
- assign p1[3]=a[3]^b[3]^c[3];
- assign g1[0]=a[0]&b[0]|a[0]&c[0]|b[0]&c[0];
- assign g1[1]=a[1]&b[1]|a[1]&c[1]|b[1]&c[1];
- assign g1[2]=a[2]&b[2]|a[2]&c[2]|b[2]&c[2];
- assign g1[3]=a[3]&b[3]|a[3]&c[3]|b[3]&c[3];
- assign sum1[0]=p1[0];
- assign sum1[1]=p1[1]^g1[0];
- assign sum1[2]=p1[2]^g1[1]^(p1[1] & g1[0]);
- assign sum1[3]=p1[3]^g1[2]^(p1[1] & g1[0] & (p1[2] | g1[1]) | p1[2] & g1[1]);
- assign co=g1[3]^((p1[1] & g1[0] & (p1[2] | g1[1]) | p1[2] & g1[1]) &
- (p1[3] | g1[2]) | p1[3] & g1[2]);
- //--> adder2
- assign p2[0]=a[0]^b[0]^d[0];
- assign p2[1]=a[1]^b[1]^d[1];
- assign p2[2]=a[2]^b[2]^d[2];
- assign p2[3]=a[3]^b[3]^d[3];
- assign g2[0]=a[0]&b[0]|a[0]&d[0]|b[0]&d[0];
- assign g2[1]=a[1]&b[1]|a[1]&d[1]|b[1]&d[1];
- assign g2[2]=a[2]&b[2]|a[2]&d[2]|b[2]&d[2];
- assign g2[3]=a[3]&b[3]|a[3]&d[3]|b[3]&d[3];
- assign sum2[0]=p2[0];
- assign sum2[1]=p2[1]^g2[0];
- assign sum2[2]=p2[2]^g2[1]^(p2[1] & g2[0]);
- assign sum2[3]=p2[3]^g2[2]^(p2[1] & g2[0] & (p2[2] | g2[1]) | p2[2] & g2[1]);
- //--> adder3
- assign p3[0]=sum1[0]^d[0];
- assign p3[1]=sum1[1]^d[1];
- assign p3[2]=sum1[2]^d[2];
- assign p3[3]=sum1[3]^d[3];
- assign g3[0]=sum1[0]&d[0];
- assign g3[1]=sum1[1]&d[1];
- assign g3[2]=sum1[2]&d[2];
- assign g3[3]=sum1[3]&d[3];
- assign sum3[0]=sum1[0]^d[0];
- assign sum3[1]=sum1[1]^d[1]^g3[0];
- assign sum3[2]=sum1[2]^d[2]^(g3[1]|(p3[1]&g3[0]));
- assign sum3[3]=sum1[3]^d[3]^(g3[2]|(p3[2]&g3[1])|(p3[2]&p3[1]&g3[0]));
- //-->mux
- assign result = co ? sum3 : sum2;
- endmodule
Это описание было специально структурировано, чтобы его можно было хоть как-то сопоставить с рисунком и понять, как оно работает. В большинстве же случаев потоковое и структурное описания очень трудно разобрать.
Поведенческое Verilog-описание этой же схемы:
- module old_scheme(
- input wire [3:0] a,
- input wire [3:0] b,
- input wire [3:0] c,
- input wire [3:0] d,
- output wire [3:0] result
- );
- wire [3:0] p1, g1, p2, g2, p3, g3;
- wire [3:0] sum1, sum2, sum3;
- wire co;
- assign {co, sum1} = a + b + c;
- assign sum2 = a + b + d;
- assign sum3 = sum1 + d;
- assign result = co ? sum3 : sum2;
- endmodule
Помимо того, что такое описание гораздо нагляднее и компактнее, оно еще и лучше синтезируется, но т.к. любая модификация исходного RTL-описания может повлечь за собой возникновение ошибок, идентичность старого и нового описаний необходимо проверить.