Записная книжка разработчика

Мои проекты

FPU. Часть 3. Делитель, RTL

| Comments

Продолжение. Начало здесь: часть 1, часть 2.

Итак, тест готов, теперь можно написать сам код делителя.
Для удобства нарисуем интерфейс модуля в таком виде:

pic1

Итак, модуль div_float производит операцию деления над двумя вещественными числами разрядностью 64 или 32 бита, которые представлены в соответствии со стандартом IEEE754. Входными сигналами модуля являются:
rst_n - сигнал сброса (инверсный)
clk - тактовый сигнал
[FLOAT_WIDTH - 1: 0] op1, op2 - делимое и делитель соответственно. FLOAT_WIDTH - параметр, указывающий разрядность, 64 или 32.
Выходы:
[FLOAT_WIDTH - 1: 0] out_reg - результат деления
done_reg - деление завершено. Сигнал выставляется в 1 на последнем такте работы делителя, и по нему следующий по схеме модуль может забирать данные с выхода.
Следующие выходы указывают на различные ситуации, возникающие при делении, их использование необязательно, но может упростить обработку исключительных ситуаций в ряде случаев. Они были рассмотрены в предыдущей части, когда мы писали тест, поэтому сейчас ограничимся их перечислением:
divizion_by_zero_reg - признак деления на ноль.
nan_reg - признак возникновения нечисла (NaN) в результате.
overflow_reg - признак переполнения.
underflow_reg - признак потери точности.
zero_reg - признак нуля в результате.

Деление происходит следующим образом. Разбиваем операнды op1 и op2 на дробную часть (мантиссу) frac1 и frac2 соответственно, экспоненту (exp1 и exp2) и знак (sign1 и sign2).

Теперь мы должны сравнить мантиссы, чтобы вычислить поправку к экспоненте:

 op2_aligned = frac1 < frac2;

И вычисляем экспоненту результата:

localparam EXP_SHIFT = (2 ** (EXP_WIDTH - 1)) - 1;

wire [EXP_WIDTH: 0] result_exp_before_correction, temp_result;

assign
temp_result = EXP_SHIFT + exp1,
result_exp_before_correction = temp_result - exp2;

always@*
begin
  if(underflow_reg) begin
    result_exp_reg = 0;
  end
  else if (overflow_reg) begin
      result_exp_reg = EXP_MAX;
    end
  else
  begin
    result_exp_reg = result_exp_before_correction - op2_aligned;
  end
end

И знак:

  wire sign1 = op1[SIGN_BIT],
  sign2 = op2[SIGN_BIT],
  result_sign = sign1 ^ sign2;

До сих пор была чистая комбинаторика, однако мантиссу результата вычислять комбинаторной схемой не имеет смысла: она получится слишком большой и медленной для практической реализации. Она вычисляется итеративно. И здесь нас ждет самый большой сюрприз. Сколько итераций нужно для вычисления 52-разрядной мантиссы 64-разрядного числа? За одну итерацию вычисляется один бит мантиссы результата. Казалось бы, из этого следует, что нам потребуется 52 итерации. Однако это не так.
Во-первых, первая (или нулевая, если считать с нуля) итерация нам нужна, чтобы загрузить регистры op1frac_stage_reg и op2frac_stage_reg, которые используются в алгоритме деления. Обратите внимание, что эти регистры намного длиннее исходных мантисс. Слева к мантиссе мы дописываем 1, в соответствии со стандартом IEEE754, а справа дописываем FRACTION_WIDTH нулей. Эти нули справа - это место куда булет сдвигаться делитель в цикле. Если их не будет, правые цифры делителя будут теряться при сдвиге, и результат получится неточным. Таким образом, для деления 52-разрядных мантисс регистры должны иметь по 105 разрядов. Также не забываем, что мантиссу делителя нужно сдвинуть ещё на 1 вправо, если она меньше мантиссы делимого. Это нужно для того, чтобы в старшем разряде результата всегда была 1:

if(div_counter_reg == 0)
begin
  op1frac_stage_reg <= {1'b1, frac1, {FRACTION_WIDTH {1'b0}}};
  op2frac_stage_reg <= (op2_aligned)? {2'b01, frac2[FRACTION_MSB: 0], {FRACTION_WIDTH - 1 {1'b0}}}: {1'b1, frac2, {FRACTION_WIDTH {1'b0}}};
end

Дополнительная итерация понадобится ещё потому, что слева к мантиссам мы дописали 1 (см. предыдущий абзац). И ещё одна итерация нужна, чтобы вычислить дополнительный бит справа. Он не попадает в результат непосредственно, но может привести к округлению младшего бита результата. Без него в некоторых случаях мы имели бы ошибку в последнем бите результата. Таким образом, нам понадобятся дополнительные три итерации, и для 52-разрядных мантисс вычисления займут 55 тактов, и на последнем, 56-м такте мы выставляем бит done_reg и собираем результат из вычисленных значений мантиссы, экспоненты и знака. Если в результате получается бесконечность, нечисло или ноль, записываем нужное значение в регистр результата.

Само деление мантисс происходит классическим "школьным" способом "в столбик" (естественно, в двоичном виде), на каждой итерации мы сравниваем мантиссу делителя и мантиссу делимого, если первая больше или равна второй, то производим их вычитание и пишем в ответ 1, если нет, то пишем ноль, после чего сдвигаем мантиссу делителя на 1 разряд вправо:

always@(negedge rst_n, posedge clk)
  begin
    if(!rst_n)
    begin
      op1frac_stage_reg <= 0;
      op2frac_stage_reg <= 0;
      div_counter_reg <= 0;
      result_frac_reg <= 0;
      done_reg <= 0;
    end
    else
    begin
      if(start)
      begin
        op1frac_stage_reg <= 0;
        op2frac_stage_reg <= 0;
        div_counter_reg <= 0;
        result_frac_reg <= 0;
        done_reg <= 0;
      end
      else 
      begin
        if(div_counter_reg == 0)
        begin
          op1frac_stage_reg <= {1'b1, frac1, {FRACTION_WIDTH {1'b0}}};
          op2frac_stage_reg <= (op2_aligned)? {2'b01, frac2[FRACTION_MSB: 0], {FRACTION_WIDTH - 1 {1'b0}}}: {1'b1, frac2, {FRACTION_WIDTH {1'b0}}};
        end
        else
        begin
          if(div_counter_reg < FRACTION_WIDTH + 3)           begin             if(op1frac_stage_reg >= op2frac_stage_reg && op1frac_stage_reg != 0)
            begin
              op1frac_stage_reg <= op1frac_stage_reg - op2frac_stage_reg;
              result_frac_reg <= {result_frac_reg[FRACTION_WIDTH - 1: 0], 1'b1};
            end
            else
            begin
              result_frac_reg <= {result_frac_reg[FRACTION_WIDTH - 1: 0], 1'b0};
            end
            op2frac_stage_reg <= op2frac_stage_reg >> 1;
          end  
          else
          begin
            if(nan_reg)
            begin
              out_reg <= NAN_VALUE;
            end
            else if(inf_out)
              begin
                out_reg <= INF_VALUE | (result_sign << SIGN_BIT);            
              end
              else if(zero_out)
                begin
                  out_reg <= 0;
                end
                else
                begin
                  out_reg <= {result_sign, result_exp_reg[EXP_WIDTH - 1: 0], result_frac_reg[FRACTION_WIDTH: 1]};
                end
            div_counter_reg <= 0;
            done_reg <= 1;
          end
        end  
        div_counter_reg <= div_counter_reg + 1'b1;  
      end
    end
  end

Сейчас мы получили делитель, который проходит набор из > 10000 тестовых воздействий, описанных в предыдущих частях, при этом результат вычислений совпадает побитно с результатом, вычисленным FPU компьютера. Это очень хорошо, но это ещё не всё.
В следующий раз мы сделаем анализ кода на время выполнения, чтобы выяснить, на какой частоте он сможет работать в конкретной FPGA. По результатам станет ясно, можно ли (и нужно ли) как-либо улучшить код.

Все исходные тексты к статье можно взять на GitHub: https://github.com/arktur04/FPU