Введение в OS-VVM (Open Source VHDL Verification Methodology)


 

I. Введение

Функциональное покрытие является одной из метрик, показывающей какая часть спецификации проверена. Качество результатов покрытия зависит от плана тестирования, т. к. 100% функциональное покрытие означает, что все функции, заданные в плане тестирования, выполнены во время моделирования. В отличии от покрытия кода, которое является функцией системы моделирования и может быть полностью автоматизировано, функциональное покрытие требует тщательной разработки тестового окружения и тестов в соответствии со спецификацией, а также анализа результатов моделирования, что не может быть полностью автоматизировано. Для осуществления функционального покрытия разработаны специальные функции в различных специализированных языках верификации, таких как SystemVerilog, ‘е’ и др.

Структура пакетов в ОS-VVM

Рис. 1. Тип protected используется в OS-VVM

Проблема верификации цифровых устройств привела к появлению методологии OS-VVM (Open Source VHDL Verification Methodology) [1, 2]. OS-VVM – это методология написания «интеллектуальных» тестирующих программ с использованием языка VHDL. Данная методология позволяет реализовать функциональное покрытие и управляемую генерацию псевдослучайных тестов, что используется при верификации цифровых функциональных блоков.

Методология OS-VVM основывается на использовании специализированных VHDL пакетов RandomPkg и CoveragePkg [2] при разработке тестирующих программ. Функции пакетов базируются на использовании защищенного типа protected (рис. 1), который появился в стандарте VHDL’2002.

II.Защищенный тип protected

Защищенный тип (protected) [3] базируется на концепции, похожей на классы в объектно-ориентированном подходе, известном из других языков программирования. Тип protected позволяет объединить данные и операции, выполняемые над ними, в один объект (инкапсуляция), таким образом, скрываются детали реализации типов данных от пользователей.

Полное определение типа protected состоит из двух частей: декларации и тела (body) типа. Объявления в декларативной части типа могут включать декларации подпрограмм (процедур и функций), спецификации атрибутов и конструкции подключения, использующие ключевое слово use. Тела подпрограмм объявляются в теле типа protected. Подпрограммы, описанные при декларации типа protected, называются методами. Ниже приведён пример декларации защищенного типа COUNTER_TYPE:

type COUNTER_TYPE is protected
  procedure Set (num : integer);
  procedure Inc;
  impure function get return integer;
end protected COUNTER_TYPE;

Элементы, объявленные внутри тела типа protected, не доступны для использования вне этого типа. Таким образом, единственный способ доступа к этим элементам, это использование методов, объявленных при декларации типа. Единственным ограничением для методов является то, что формальные параметры методов не могут быть типа access или file.

Тело типа protected задаёт детали реализации данного типа, в теле типа могут быть описаны: декларации и тела подпрограмм, пакетов; декларации типов, подтипов, констант, переменных, файлов и alias (переименований); декларации атрибутов, спецификации и др. Пример тела защищенного типа COUNTER_TYPE:

type COUNTER_TYPE is protected body 
  variable count : integer := 0;
  procedure Set ( num : integer) is
  begin
     count := num ;
  end procedure Set;
  procedure Inc is
  begin
    count := count + 1 ;
  end procedure Inc;
  impure function Get return integer is
  begin
    return count;
  end function Get;
end protected body COUNTER_TYPE;

Только локальные либо общие (shared) переменные могут быть типа protected. Передача значения одной переменной типа protected другой переменной не допускается. Как следствие, переменная защищенного типа не должна иметь присвоения начального значения при декларации. Аналогичным образом, операторы отношений (например, равенства (“=”) и неравенства (“/=”)) не могут использоваться для переменных защищенного типа.

Для того чтобы вызвать методы (функции, процедуры) защищенного типа, нужно указать имя переменной и имя метода, разделенные точкой, например, если переменная объявлена как:

shared variable Cnt : COUNTER_TYPE;

тогда она может использоваться так:

if (Cnt.Get = 6) then . .

В выражении выше вызывается метод Get переменной Cnt, который возвращает значение внутренней переменной count.

Использование защищённых типов в OS-VVM защищает пользователя от довольно сложных структур и подпрограмм, поддерживающих генерацию псевдослучайных тестов и функциональное покрытие.

III. Пакет RandomPkg

В пакете RandomPkg декларируется защищенный тип RandomPType, который включает в себя начальное значение (seed) псевдослучайного генератора и набор функций для генерации случайных чисел в различных форматах и диапазонах. Генерация псевдослучайных чисел с использованием типа RandomPType проходит в три этапа: декларация переменной данного типа, настройка генератора (в простейшем случае задание начального значения seed) и получение псевдослучайного числа, как показано в следующем примере.

-- декларация переменной RV
variable RV : RandomPType;
...
-- задание начального значения seed
RV.InitSeed(RV’instance_name);
X <= RV.RandInt(1, 10); -- получение
-- псевдослучайного числа в диапазоне [1, 10]

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

RndGenProc : process
-- защищённый тип из RandomPkg
 variable RV : RandomPType ;
 variable D  : integer ;
begin
-- Задание значения seeds
RV.InitSeed(RV'instance_name);
-- Получение значения 
-- из диапазона [0, 255]
D := RV.RandInt(0, 255);
 . . .
-- Получение значения в
-- диапазоне [1, 9], исключая
-- значения 2, 4, 6, 8
D := RV.RandInt(1, 9, (2, 4, 6, 8));
 . . .
-- Получение значения из
-- набора чисел 1, 3, 7, 9.
D := RV.RandInt( (1, 3, 7, 9) );
 . . .
-- Получение значения из
-- набора 1, 3, 7, 9, исключая
-- значения 3, 7
D:=RV.RandInt((1, 3, 7, 9), (3, 7));

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

В пакете RandomPkg описан также ряд функций генерации псевдослучайных чисел, имеющих упрощенный вызов (сокращенный набор указываемых при вызове входных аргументов) [4].

Функции генерации псевдослучайных чисел доступны не только для целых чисел, но и для векторных типов std_logic_vector (метод RandSlv), unsigned (RandUnsigned) и signed (RandSigned). При этом значения параметров по-прежнему задаются как целые числа (integer), но при вызове этих функций нужно указать еще дополнительный параметр – разрядность генерируемого вектора.

. . .
variable DataSlv :
  std_logic_vector(7 downto 0);
begin
. . .
-- Получение значения
-- из диапазона [0, 255]
DataSlv := RV.RandSlv(0, 255, 8);

По умолчанию, функции генерации возвращают псевдослучайные числа, подчиняющиеся равномерному закону распределения (реализованному с помощью процедуры uniform пакета math_real из библиотеки IEEE). В пакете реализованы другие законы распределения: FAVOR_SMALL (распределение с преобладанием малых значений), FAVOR_BIG (распределение с преобладанием больших значений), NORMAL (нормальный закон распределения, закон Гаусса), POISSON (распределение Пуассона). Получить другой закон распределения можно двумя способами. Первый – это использовать метод SetRandomParm для задания закона распределения по умолчанию, например:

RV.SetRandomParm(NORMAL, 5.0, 2.0);

В этом случае все функции генерации псевдослучайных чисел (Rand*) будут возвращать числа, подчиняющиеся нормальному распределению. В случае, если нужно генерировать только целые псевдослучайные числа, можно использовать перегруженные функции Uniform, FavorSmall, FavorBig, Normal, Poisson, которые возвращают псевдослучайные значения, подчиняющихся соответствующим законам распределения, независимо от закона распределения, заданного по умолчанию.

Кроме стандартных законов распределения случайной величины в пакете существует возможность взвешенной генерации – так называют [5] генерацию по произвольному закону распределения с помощью указания списка требуемых значений целых чисел и их вероятностей появления (весов). Для взвешенной генерации псевдослучайных чисел в типе RandomPType есть две группы перегруженных функций DistVal* и Dist*. При вызове функций группы DistVal* в качестве аргумента задаётся массив пар чисел (число, вес). При вызове функций из другой группы Dist* задаётся массив (integer_vector) весов. Например, функция DistValInt вызывается с массивом пар значений.

Data := RV.DistValInt( ((1, 7), (3, 2), (5, 1)) );

Первый элемент в паре это значение, а второй – его вес. Частота, с которой каждое значение будет возникать, зависит от вероятности, которая определяется по формуле [вес/(сумма всех весов)]. В приведённом примере в результате многократного вызова метода DistValInt появление числа 1 будет с вероятностью 7/10 или 70%, числа 3 – 20%, а числа 5 – 10%.

Функция DistInt является упрощённой версией DistValInt, в которой задаются только веса. Числа генерируются в диапазоне от 0 до N – 1, где N –количество заданных весов. Например, результат многократного вызова

Data := RV.DistValInt( ((1, 7), (3, 2), (5, 1)) );

функции DistInt будет следующим: вероятность выпадения числа 0 будет 70%, числа 1 – 20%, а числа 2 – 10%.

IV. Пакет CoveragePkg

Пакет CoveragePkg включает описание новых типов данных и функций, которые позволяют создавать корзины для точек покрытия и перекрестного покрытия, собирать контролируемые значения переменных (сигналов), проверять полноту покрытия, выводить отчет о результатах покрытия. Но наиболее важной функцией пакета CoveragePkg является возможность организации интеллектуального покрытия (intelligent coverage), под которым понимается выбор псевдослучайного значения (или набора значений для перекрестного покрытия) из диапазона непокрытых значений [1, 2].

В методологии OS-VVM функциональное покрытие осуществляется сбором значений переменных и сигналов VHDL проекта при выполнении моделирования.

В табл. 1 приведены основные этапы работы с функциями пакета CoveragePkg. Для организации покрытия в VHDL-программе необходимо создать переменную защищенного типа CovPType и описать модель покрытия путем задания необходимых корзин. Далее в соответствии с планом тестирования с помощью метода ICover осуществляется сбор значений переменных или сигналов в заданные моменты времени (или по определенным событиям). Полученные данные о покрытии можно использовать для управления псевдослучайной генерацией тестов при использовании метода RandCovPoint. Метод IsCovered позволяет проверить достигнуто ли полное покрытие всех корзин. С помощью методов WriteCovDb и ReadCovDb можно сохранить и загрузить базу данных о покрытии, что позволяет осуществить объединение данных о покрытии по нескольким запускам моделирования, которые могут выполняться параллельно. Далее рассмотрим этап создания корзин более подробно.

Таблица 1 – Основные этапы работы с функциями пакета CoveragePkg

Подключение пакета

use work.CoveragePkg.all;

Декларация объекта покрытия

shared variable CovX, CovXY : CovPType;

Генерация корзин

GenBin(0, 7); — 8 корзин, 1 значение в каждой

 

GenBin(0, 255, 16); — 16 корзин одинакового размера}

Создание точек покрытия, либо
перекрестного покрытия

CovX.AddBins(GenBin(0, 31, 8));

CovX.AddBins(GenBin(32, 47, 1));

CovXY.AddCross(GenBin(0, 7), GenBin(0, 7));

Выборка (сбор) значений

CovX.ICover(X);

Проверка полноты покрытия

if CovX.IsCovered then

Оценка непокрытой области

NotCov := CovX.CountCovHoles;

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

X := CovX.RandCovPoint; — выбор непокрытых значений

Вывод отчета

CovX.WriteBin; — отчет может быть достаточно большим

Сохранение базы данных

CovX.WriteCovDb(“covdb.txt”, OpenKind => WRITE_MODE);

 

Создание корзин (модели покрытия)

Функциональное покрытие осуществляется распределением значений переменных проекта по заранее определённым корзинам (bins) – диапазонам значений, которые имеют специальное назначение в проекте. Для корзин, использующих только одну переменную, создается структура данных, которую называют элементом покрытия (coverage item) или точкой покрытия (coverage point). По корзинам перекрёстного покрытия (cross coverage bins) распределяются пары (тройки и т.д.) значений двух либо нескольких переменных. Для корзин, использующих две и более переменные, создается структура данных, называемая элементом перекрёстного покрытия (cross coverage item).

В пакете CoveragePkg для корзины определена запись CovBinBaseType со следующими полями: BinVal – массив, включающий минимальное и максимальное значения интервала собираемых корзиной значений; Count – текущее (при моделировании) число попаданий в корзину значений из диапазона BinVal; AtLeast – цель покрытия задаёт число попаданий, при котором корзина будет считаться покрытой; Weight – вес – это целое (не равное нулю) положительное число, по которому вычисляется вероятность выбора корзины при использовании метода RandCovPoint; Action – действие (корзина может быть запрещенной, игнорируемой или рабочей), для рабочей корзины считается число попаданий. Тип CovBinType представляет собой массив записей CovBinBaseType. Создание корзин (тип CovBinType) осуществляется вызовом функции GenBin. Метод AddBins используется для последовательного добавления созданных корзин в структуру данных покрытия – переменную, заданную в защищенном типе CovPType.

Функция GenBin является перегруженной и может вызываться с числом целочисленных аргументов от одного до пяти: AtLeast – цель покрытия, Weight – вес, Min – нижняя граница диапазона, Max – верхняя граница диапазона, NumBin – число корзин, на которое нужно разделить диапазон [Min, Max]. Функция GenBin возвращает тип CovBinType. Например, следующая команда создает три корзины с диапазонами собираемых значений [1, 2], [3, 4], [5, 6], устанавливает для каждой корзины цель покрытия равную 3, а метод AddBins добавляет корзины в структуру данных покрытия.

--                AtLeast  Min  Max  NumBin
CovX.AddBins( GenBin(  3,   1,   6,   3) );

При вызове функции GenBin с двумя аргументами (Min, Max) будет создано N корзин, где N = MaxMin + 1. Следующие три вызова функции GenBin являются эквивалентными и создают одну корзину с диапазоном собираемых значений [7, 7], значение цели покрытия и веса для данной корзины устанавливаются равными 1.

CovX.AddBins( GenBin(5) );
CovX.AddBins( GenBin(5, 5) );
CovX.AddBins( GenBin(5, 5, 1) );

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

При создании корзин, иногда необходимо обозначить их так, чтобы при попадании значений в них реагировать как на ошибки или игнорировать и не считать их. Перегруженные функции IllegalBin и IgnoreBin используются для создания запрещённых и игнорируемых корзин и могут вызываться с одним, двумя либо тремя аргументами. При вызове функций IllegalBin и IgnoreBin с тремя аргументами задаются: минимальное (Min) и максимальное (Max) значения диапазона, и число корзин (NumBin) на которое разбивается заданный диапазон.

-- 3 запрещённые корзины: [1,3], [4,6], [7,9]
CovX.AddBins( IllegalBin(1, 9, 3) );
-- 1 запрещенная корзина [1,9]
CovX.AddBins( IllegalBin(1, 9, 1) );
-- 3 игнорируемые корзины: [1,1], [2,2], [3,3]
CovX.AddBins( IgnoreBin ( 1, 3, 3) );

Вызов функций IgnoreBin и IllegalBin с двумя параметрами (Min, Max) приведёт к созданию одной корзины с диапазоном [Min, Max]. При вызове данных функций с одним параметром создаётся одна корзина с единичным диапазоном собираемых значений.

Так как все функции GenBin, IllegalBin и IgnoreBin возвращают значение типа CovBinType, их результаты могут быть объединены с помощью оператора конкатенации. В приведенном ниже примере будут добавлены шесть допустимых корзин [0, 0], [1, 1], [2, 5], [6, 9], [14, 14], [15, 15], одна игнорируемая корзина [10, 13], а все остальные корзины попадают в запрещённую корзину.

CovX.AddBins(
 GenBin(0,1) & GenBin(2,9,2) & GenBin(14,15)
 & IgnoreBin(10, 13)
 & ALL_ILLEGAL);

Кроме приведенной константы ALL_ILLEGAL, означающей запрещенную корзину с диапазоном значений [integer’left, integer’right], в пакете CoveragePkg декларируются следующие константы: ALL_BIN и ALL_COUNT – являются синонимами и обозначают рабочую корзину [integer’left, integer’right], ALL_IGNORE – игнорируемая корзина [integer’left, integer’right], ZERO_BIN – рабочая корзина [0, 0], ONE_BIN – рабочая корзина [1, 1]. Порядок добавления корзин в структуру данных покрытия имеет значение, т.к. по умолчанию установлен режим COUNT_FIRST, при котором собираемое значение распределяется в первую подходящую корзину. Чтобы задать режим, при котором собираемое значение распределяется по всем подходящим корзинам, необходимо вызвать метод SetCountMode с параметром COUNT_ALL.

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

CovXY.AddCross( GenBin(0,3), GenBin(0,3) );

Метод AddCross создаёт векторное произведение корзин, заданных в качестве входных аргументов. Каждый вызов GenBin(0,3) создаёт четыре корзины: 0, 1, 2, 3. В результате чего AddCross создаёт 16 корзин со следующими парами: (0,0), (0,1), (0,2), (0,3), (1,0), (1,1), (1,2), (1,3), … , (3,0), (3,1), (3,2), (3,3). В приведённом примере используется перекрестное покрытие между двумя элементами, но метод AddCross поддерживает пересечение до 20 элементов.

Метод ICover используется для сбора покрытия. Для точек покрытия, метод ICover принимает значение integer. Для перекрёстного покрытия, метод ICover принимает значение integer_vector. Примеры вызова метода приведены ниже. Для обозначения типа integer_vector требуется вторая пара круглых скобок.

CovX.ICover( X );
CovXY.ICover( (X, Y) );

Процедуры WriteBin и WriteCovHoles используются для вывода отчета о покрытии.

procedure WriteBin ;
procedure WriteBin (FileName : string; OpenKind : File_Open_Kind := APPEND_MODE) ;
procedure WriteCovHoles ( PercentCov : real := 100.0 ) ;
procedure WriteCovHoles ( FileName : string;
   PercentCov : real := 100.0 ;
   OpenKind : File_Open_Kind := APPEND_MODE );

Метод WriteBin печатает (выводит) результаты покрытия с выводом одной корзины на строку. Есть две версии. Первая не имеет аргументов и выводит в стандартный поток OUTPUT. Это показано ниже. Следует обратить внимание, что корзины, отмеченные как игнорируемые не выводятся WriteBin, а корзины отмеченные как запрещённые выводятся только если они имеют не нулевые значения счетчика (счёта).

ReportCov : process
begin
 wait 
  until rising_edge(Clk) and CovX.IsCovered ;
 CovX.WriteBin ;
 wait ;
end process ;

Другая версия метода принимает два аргумента. Первый аргумент FileName задаёт имя файла (тип string). Второй аргумент задаёт значение OpenKind (to file_open) и принимает одно из значений WRITE_MODE или APPEND_MODE. Аргумент OpenKind инициализируется в значение APPEND_MODE.

--                FileName,    OpenKind
CovBin1.WriteBin ("Test1.txt", WRITE_MODE);

Метод WriteCovHoles выводит результаты покрытия, которые ниже параметра PercentCov. Следует обратить внимание, что корзины, помеченные как запрещённые или игнорируемые, не выводятся методом WriteCovHoles. Параметр ProcentCov инициализируется в 100% и обычно таким и остается.

CovX.WriteCovHoles ;

Другая версия WriteCovHoles задаёт FileName, PercentCov, и OpenKind в похожем стиле метода WriteBin. Аргумент инициализируется в APPEND_MODE. Это показано ниже.

--                 FileName,    PercentCov, OpenKind
CovX.WriteCovHoles("Test1.txt", 100.0,      APPEND_MODE);

Методы SetName и SetItemName используютя для печати первой и второй строки заголовка для методов WriteBin и WriteCovHoles. SetName предназначается для ссылки на имя или цель корзины покрытия. SetItemName предназначена для вывода столбцов заголовка для корзин покрытия. Каждая из них также использует их строковые параметры для инициализации внутреннего начального значения(seed) для псевдослучайного генератора чисел.

 

V. Использование пакетов

В примере демонстрируется использование пакетов RandomPkg и CoveragePkg. Заданы два сигнала целочисленного типа, принимающие значения из диапазонов [0, 3] и [0, 4] соответственно, необходимо сгенерировать все возможные пары значений не менее двух раз. В программе декларируются две пары сигналов X, Y и A, B. Для генерации значений X, Y используется метод RandInt из пакета RandomPkg, а для A, B – метод RandCovPoint из пакета CoveragePkg для проведения интеллектуального покрытия.

library ieee;
use work.RandomPkg.all;
use work.CoveragePkg.all;
entity example is
end entity example;
architecture beh of example is
 signal X, Y, A, B : natural;
 shared variable CrossXY, CrossAB : CovPType;
begin
 p1: process is
  variable RndX : RandomPtype;
  variable RndY : RandomPtype;
 begin
  CrossXY.AddCross(2,GenBin(0,3),GenBin(0,4));
  RndX.InitSeed(RndX'instance_name);
  RndY.InitSeed(RndY'instance_name);
  l1: while not CrossXY.IsCovered loop
   X <= RndX.RandInt(0,3);
   Y <= RndY.RandInt(4);
   wait for 10 ns;
   CrossXY.ICover((X, Y));
  end loop;
  CrossXY.WriteBin; 
  wait;
 end process p1;
 p2: process is
 begin
  CrossAB.AddCross(2,GenBin(0,3),GenBin(0,4));
  l1: while not CrossAB.IsCovered loop
    (A, B) <= CrossAB.RandCovPoint;
    wait for 10 ns;
    CrossAB.ICover((A, B));
  end loop;
  CrossAB.WriteBin; 
  wait;
 end process p2;
end architecture beh;

Для каждой из пар сигналов (X, Y и A, B) собирается перекрестное покрытие с помощью общих переменных CrossXY и CrossAB. В результате моделирования для покрытия всех пар значений X, Y дважды потребовалось 105 циклов, а для покрытия A, B – 40 циклов. В табл. 2 приведены результаты покрытия пары X, Y, в табл. 3 – пары A, B. Из табл. 2 видно, что при использовании псевдослучайного генератора с линейным законом распределения (uniform) распределение пар значений X, Y не равномерно. Пара чисел (2, 3) генерировалась 10 раз, а пара (0, 1) – 2 раза.

Таблица 2 – Распределение пар значений X, Y

Значения

X

Значения Y

0

1

2

3

4

0

6

2

6

4

3

1

4

6

3

6

7

2

6

5

3

10

6

3

9

6

4

3

6

 

Таблица 3 – Распределение пар значений A, B

Значения A

Значения B

0

1

2

3

4

0

2

2

2

2

2

1

2

2

2

2

2

2

2

2

2

2

2

3

2

2

2

2

2

 

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

Заключение

Пакеты RandomPkg и CoveragePkg, входящие в методологию OS-VVM, существенно расширяют возможности функциональной верификации с помощью языка VHDL, упрощая процесс разработки тестирующих программ, использующих управляемую генерацию псевдослучайных тестов и функциональное покрытие. Исходные тексты пакетов доступны в сети Internet, продолжают обновляться и хорошо документированы. В последних версиях системы моделирования Questasim фирмы Mentor Graphics появилась библиотека OS-VVM, в которой скомпилированы пакеты RandomPkg и CoveragePkg.

Литература

[1] Open source VHDL verification methodology. User’s Guide Rev. 1.2 [Electronic resource] / Ed. J. Lewis. – Mode of access: http://osvvm.org/downloads. – Date of access: 02.09.2013.

[2] Авдеев, Н. А. Средства VHDL для функциональной верификации цифровых систем. Методология OS-VVM. / Н. А. Авдеев, П. Н. Бибило // Современная электроника. – 2013. – № 5. – С. 66-70.

[3] Авдеев, Н. А. Средства VHDL для функциональной верификации цифровых систем / Н. А. Авдеев, П. Н. Бибило // Современная электроника. – 2013. – № 3. – С. 74-76.

[4] Авдеев, Н. А. Средства VHDL для функциональной верификации цифровых систем: пакет RandomPkg / Н. А. Авдеев, П. Н. Бибило // Современная электроника. – 2014. – № 1. – С. 60-65.

[5] Проектирование и верификация цифровых систем на кристаллах. Verilog & SystemVerilog / В. И. Хаханов [и др.]. – Харьков : ХНУРЭ, 2010. – 528 с.

Контактная информация:

Автор идеи и контента: Бибило П.Н.
Разработчики: Голанов В.А., Зарембо Д.В.
На основе Wordpress CMS

Статистика за сегодня:

Сайт размещен на сервере ОИПИ НАН Беларуси