Фев 28 2015

FPU. Часть 2. Тест делителя

Итак, продолжим разработку FPU.

Первая часть статьи находится здесь: http://32bit.me/?p=1902. Напомню также, что все исходники, приведенные в статье, можно скачать на гитхабе.

waveform-1

Картинка для привлечения внимания (кликабельно)

Мы начали сразу с самого сложного в реализации арифметического действия, деления. В прошлый раз была написана программа на С++, генерирующая тестовый файл для делителя. Напомню, что представляет собой этот файл. Файл содержит тройки 64-битных чисел в шестнадцатиричном формате: делимое, делитель и результат. Каждая строка заканчивается комментарием, содержащем те же числа в обычном виде:

2d4e8d5ab04c9587 a6b8642320878ccd c6840a9d8fb71e67 //1.87479e-090 / -3.68973e-122 = -5.08112e+031
d5a54167e94108ad 80b1de60b13de799 7ff0000000000000 //-3.80855e+104 / -2.54458e-305 = +inf
b32f930421d14cfe 260042159f18e49f cd1f12ad1a47c247 //-3.83764e-062 / 1.20089e-125 = -3.19567e+063
ce95a1d3cfd56fca 7eaf8a434053738f 8fd5f294324a3345 //-3.73249e+070 / 1.68977e+302 = -2.20887e-232
69c0c9b4a665bea1 ad07f62a2765be5a fca66b76439c882c //2.57006e+201 / -9.18982e-092 = -2.79664e+292
0bd8a6c89ddbc656 178f014d2c093432 3439714a082fc83f //1.34495e-251 / 3.31822e-195 = 4.05323e-057

В начале файла идут «особые» значения, все возможные комбинации из +inf, -inf, +0, -0, nan, затем идет 10000 (десять тысяч) комбинаций случайных вещественных чисел.
Тест (на Verilog) должен построчно считывать эти значения и подавать операнды на вход DUT (Design Under Test, модуль, подвергаемый тестированию), а затем сравнивать выход DUT с значением, прочитанным из файла.
Заметим, что мы начали разработку именно с теста, а не с самого делителя. Такой подход является наиболее правильным, и позволяет продумать нюансы реализации DUT до того, как мы начнём писать его код.

Какова разрядность операндов? Стандарт IEEE 754, который мы должны соблюдать, определяет 4 вида представления чисел с плавающей точкой: 16, 32, 64 и 128 бит (а также некие «децимальные» форматы, которые мы обсуждать не будем). На практике же применяются только 32 и 64 бита, т.к. 16-битный формат имеет слишком низкую точность для практических задач, а 128-битный — слишком высокую, и, следовательно, слишком много ресурсов будет тратиться впустую на вычисления ненужных знаков.
Мы сделаем наш модуль параметризируемым, с возможностью выбора 32 или 64-битного формата, по умолчанию пусть будет 64 бита. Имя параметра будет FLOAT_WIDTH.

Начнём с определения интерфейса DUT, то есть всей совокупности его входов и выходов. Входами, очевидно, будут операнды (op1 и op2 разрядностью FLOAT_WIDTH), и сигнал start, по которому модуль начинает работу. Выходом будет сигнал out_reg, разрядностью FLOAT_WIDTH. Суффикс _reg означает, что выход является регистровой переменной (я придерживаюсь определенных правил кодирования и именования сигналов, это одно из них). Также будут следующие однобитовые выходы: divizion_by_zero_reg, nan_reg, overflow_reg, underflow_reg, zero_reg, done_reg.

Обратите внимание, что все выходы являются регистрами. Так рекомендуют делать учебники, и на это есть некоторые причины. Выход done_reg сигнализирует о том, что модуль закончил расчёт и выставил на выход валидный ответ.
divizion_by_zero_reg — деление на ноль. Возникает, если делитель равен нулю, и при этом делимое не равно нулю.
nan_reg — Ответ равен NaN (нечисло). Примеры: 0 / 0, inf / inf, nan / 2;
overflow_reg — переполнение разрядной сетки порядка, результат равен +/-inf;
underflow_reg — потеря точности (антипереполнение разрядной сетки порядка), результат равен нулю с соответствующим знаком. Не возникает, если результат в точности равен нулю (например, 0 / 1);
zero_reg — результат равен нулю (+0 или -0);

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

И, разумеется, DUT будет иметь стандартные входы clk — тактовый сигнал b rst_n — сброс. Суффикс _n здесь и далее будет обозначать инверсный сигнал, это тоже одно из моих правил кодинга на Verilog.

Сам модуль будет называться div_float.

Итак, мы можем записать:

div_float dut(
  //inputs
  .rst_n(rst_n),
  .clk(clk),
  .start(start),
  .op1(op1),
  .op2(op2),
  //outputs
  .out_reg(out_actual),
  .divizion_by_zero_reg(divizion_by_zero_actual),
  .nan_reg(nan_actual),
  .overflow_reg(overflow_actual),
  .underflow_reg(underflow_actual),
  .zero_reg(zero_actual),
  .done_reg(done));

Теперь напишем остальной тест.

`timescale 1ns/1ns

module fpu_tb();

`define DATA_WIDTH 64
`define EXP_WIDTH 11
`define FRAC_WIDTH (`DATA_WIDTH - `EXP_WIDTH - 1)
`define MAX_LINE_LENGTH 1000 
`define NaN 64'h7ff8000000000000

reg clk, rst_n, start;
integer test_file, status, i;
reg [`DATA_WIDTH - 1: 0] op1, op2;
reg [`DATA_WIDTH - 1: 0] out_expected;
reg divizion_by_zero_expected, nan_expected, overflow_expected, underflow_expected, zero_expected;
wire [`DATA_WIDTH - 1: 0] out_actual;
wire divizion_by_zero_actual, nan_actual, overflow_actual, underflow_actual, zero_actual, done;
reg [`MAX_LINE_LENGTH * 8: 1] comment;
reg error;

always #2 clk = ~clk;

Некоторые пояснения. Сигналы clk, rst_n, start, op1, op2 — входные сигналы модуля div_float.
test_file, status — идентификатор файла и состояние файловой операции
i — просто вспомогательная переменная, показывающая номер итерации. Без неё можно обойтись.
comment — текст комментария, заполняется при парсинге строки
out_expected, reg divizion_by_zero_expected, nan_expected, overflow_expected, underflow_expected, zero_expected — суффикс _expected указывает на то, что это ожидаемые значения, те значения, которые мы должны получить на выходе модуля на данной итерации.
wire divizion_by_zero_actual, nan_actual, overflow_actual, underflow_actual, zero_actual — суффикс _actual указывает на то, что это реальные значения, те значения, которые фактически получены на выходе модуля на данной итерации.
error — сигнал ошибки. При несовпадении сигнала _expected и сигнала _actual принимает значение 1 и остаётся в нём до конца теста.

initial
begin
  error = 0;
  clk = 1'b0;
  rst_n = 1'b0;
  start = 0;
  #10 rst_n = 1'b1;
  //------------
  test_file = $fopen("test_file.txt", "r");  // открываем файл
  i = 0;
  while ( ! $feof(test_file)) //цикл до конца файла
  begin  
    @(negedge clk); 
    status = $fscanf(test_file,"%h %h %h", op1, op2, out_expected); // парсим очередную строку
    status = $fgets(comment, test_file); // пропускаем комментарий
      // вычисляем значения флагов
      divizion_by_zero_expected = !is_zero(op1) && is_zero(op2);  
      nan_expected = is_nan(out_expected);
      overflow_expected = is_inf(out_expected) && !is_inf(op1) && !is_zero(op2);
      underflow_expected = is_zero(out_expected) && !is_zero(op1) && !is_inf(op2);
      zero_expected = is_zero(out_expected);
      //--------
      i = i + 1;
      start = 1;
      @(negedge clk);
      start = 0;
      //ждем, когда DUT завершит вычисление
      @(posedge done);
      // сравнение ответа с ожиданием
      if(!is_equal(out_expected, out_actual))
        error = 1;
      if((divizion_by_zero_expected != divizion_by_zero_actual) || (nan_expected != nan_actual) || (overflow_expected != overflow_actual) || (underflow_expected != underflow_actual) || (zero_expected != zero_actual))
        error = 1;
  end
  //закрываем файл и прекращаем работу
  $fclose(test_file);
  #100 $stop; 
end

Здесь используются дополнительные функции is_equal, is_zero, is_inf, is_nan.
Функция is_equal сравнивает два числа побитно, если они не являются NaN, и считает числа равными, если оба являются NaN, даже если они не совпадают побитно.

function is_equal;
input [`DATA_WIDTH - 1: 0] expected, actual;
reg expectedIsNaN, actualIsNaN;
begin
  expectedIsNaN = &(expected | ~`NaN);
  actualIsNaN = &(actual | ~`NaN);
  is_equal = (expected == actual) || (expectedIsNaN && actualIsNaN);
end
endfunction

Функции is_zero, is_inf и is_nan возвращают единицу, если аргумент равен нулю, бесконечности и NaN соответственно.

function is_zero;
input [`DATA_WIDTH - 1: 0] value;
begin
  is_zero = value[`DATA_WIDTH - 2: `DATA_WIDTH - `EXP_WIDTH - 1] == {`EXP_WIDTH{1'b0}};
end
endfunction
function is_inf;
input [`DATA_WIDTH - 1: 0] value;
begin
  is_inf = (value[`DATA_WIDTH - 2: `DATA_WIDTH - `EXP_WIDTH - 1] == {`EXP_WIDTH{1'b1}}) && (value[`DATA_WIDTH - `EXP_WIDTH - 2: 0] == {`FRAC_WIDTH{1'b0}});
end
endfunction
function is_nan;
input [`DATA_WIDTH - 1: 0] value;
begin
  is_nan = (value[`DATA_WIDTH - 2: `DATA_WIDTH - `EXP_WIDTH - 1] == {`EXP_WIDTH{1'b1}}) && (value[`DATA_WIDTH - `EXP_WIDTH - 2: 0] != {`FRAC_WIDTH{1'b0}});
end
endfunction

В следующей статье мы напишем сам модуль div_float.

Ссылка на github:  https://github.com/arktur04/FPU