Апр 23 2015

FPU. Часть 4. Умножитель, тест и RTL

Предыдущая часть здесь.

pic

Продолжим разработку FPU. В предыдущих частях был рассмотрен делитель вещественных чисел Деление является самым сложным в реализации арифметическим действием. Следующим по сложности идёт сложение, а самой простой операцией является умножение. В этой части рассмотрен умножитель.

Тест умножителя полностью аналогичен тесту делителя. Напоминаю, что в данном проекте тест основан на текстовом файле, в котором записаны тройки чисел: операнды и результат. Текстовый файл формируется специальной программой, исходный текст которой (на C++) прилагается к проекту. В тестовом наборе присутствуют все комбинации «особых» значений, таких как NaN, +/-Inf, +/-0 и десять тысяч случайных пар операндов.

В самом умножителе нет ничего сложного, для умножения двух вещественных чисел в формате IEEE 754 мы должны перемножить мантиссы чисел и сложить экспоненты. При перемножении мантисс целая часть результата может превышать 1, поэтому для целой части результата мы вводим дополнительный разряд и анализируем его после умножения. Если в этом разряде 1, то есть целая часть результата >= 2, мы жолжны нормализовать результат, то есть сдвинуть его на 1 разряд вправо, и скорректировать экспоненту, прибавив к ней 1.

Экспонента результата представляет собой сумму экспонент операндов минус величина сдвига экспоненты (1023 для 64-битного формата) плюс величина коррекции, 1 или 0, в зависимости от результата умножения мантисс. Почему нужно вычитать величину сдвига и что это вообще такое?

В стандарте IEEE 754 экспонента числа записывается со со сдвигом. Т.е., если экспонента равна нулю, то в разряды экспоненты будет записано 1023, или 0x3ff (здесь и далее значения для 64-битных чисел). Если экспонента равна -1022, то в разряды экспоненты будет записано число 1, если экспонента равна 1023, то будет записано число 2066, или 0x7fe. То. есть, exp = exp_signed + shift, где exp — содержимое разрядов экспоненты, exp_signed — значение экспоненты, shift — константа сдвига. Для того, чтобы после сложения экспонент получился верный результат, из суммы нужно вычесть константу сдвига:

exp_result = exp1 + exp2 — shift = (exp_signed1 + shift) + (exp_signed2 + shift) — shift = exp_signed1 + exp_signed2 — shift .

Если в разрядах константы содержится ноль, то число, в соответствии со стандартом IEEE 754 является денормализованным. Наш FPU не обрабатывает денормализованные числа, считая их нулями. На практике, денормализованным числам соответствуют числа, имеющие порядок 2^-1023, т.е. 10^-308. Эти числа настолько малы, что очень редко применяются на практике, и для упрощения FPU их не обрабатывают в большинстве реализаций FPU.

Если в разрядах экспоненты содержится 3ff, это является признаком бесконечности (Inf)  или нечисла (NaN), и обрабатывается отдельной логикой.

При сложении экспонент мы считаем результат с двумя дополнительными разрядами. Это нужно для того, чтобы различать ситуации переполнения и антипереполнения. При переполнении в старшем разряде будет 0, в следующем за ним будет единица. Пример:

0x7fe + 0x7fe — 0x3ff = 0xbfd= 0_1011_1111_1101

При переполнении мы считаем, что результат равен Inf, и выставляем выходной сигнал overflow.

При антипереполнении в старшем разряде будет единица, то есть экспонента меньше -1023:

0x001 + 0x001 — 0x3ff = 1_1100_00_0000_0011

При антипереполнении мы считаем, что результат равен нулю, и выставляем выходной сигнал underflow.

Ситуации, когда один или оба операнда равны Inf, NaN или нулю, обрабатываются отдельной логикой.

Умножитель работает в три такта: на такте 0 вычисляется сумма экспонент без коррекции и произведение мантисс, на такте 1 вычисляется коррекция экспоненты, и, при необходимости, сдвиг мантиссы, на такте 2 вычисляется мантисса с учётом округления, и значения выходных флагов nan_reg, overflow_reg, underflow_reg, zero_reg.

Мантисса вычисляется путём простого умножения: frac1 * frac2, что может потребовать нескольких аппаратных умножителей в составе FPGA. Если это недопустимо, по тем или иным причинам, данную операцию можно разбить на несолько тактов, так, чтобы использовался только один аппаратный умножитель. Вообще, операцию умножения довольно легко ковейеризировать, существенно повысив при этом общее быстродействие системы.

Ниже приведены фрагменты кода, отвечающие за такты 0, 1:

	   if(stage_reg == 0)
		begin
	     full_exp_sum_reg <= exp1 + exp2 - EXP_SHIFT;
	     full_frac_reg <= frac1 * frac2; 
		end
		else if(stage_reg == 1)
		begin
		  //exp correction must be undertaken
		  full_exp_sum_after_correction_reg <= full_exp_sum_reg + full_frac_reg[PRODUCT_WIDTH - 1];
		  frac_res_before_rounding_reg <= full_frac_reg[PRODUCT_WIDTH - 1]? full_frac_reg[PRODUCT_WIDTH - 1: PRODUCT_WIDTH - FRACTION_WIDTH - 2] : full_frac_reg[PRODUCT_WIDTH - 2: PRODUCT_WIDTH - FRACTION_WIDTH - 3];
		end

И за такт 2:

if(stage_reg == 2)
    begin
	   if(is_nan_result)
		begin
		  out_reg <= NAN_VALUE;
		end
		else if(is_zero_result)
		begin
		  out_reg <= {sign_res, {(FRACTION_WIDTH + EXP_WIDTH){1'b0}}};
		end
		else if(is_inf_result)
		begin
		  out_reg <= {sign_res,  {EXP_WIDTH{1'b1}}, {FRACTION_WIDTH{1'b0}}};
		end
		else
		begin
	     out_reg <= {sign_res, exp_res, frac_res};
		end
	 end
	 begin
      if(stage_reg == 2)
		begin
		  nan_reg <= is_nan_result;
        overflow_reg <= is_overflow_result;
        underflow_reg <= is_underflow_result;
        zero_reg <= is_zero_result;
		end
    end

Код проекта доступен на Github: https://github.com/arktur04/FPU