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

Существующие решения


Для начала посмотрим, как там у флагманов.
Intel использует SRT Radix-4 или 16, там деление делается столбиком, сами числа разбиты на разряды по несколько бит, на каждой итерации в результат отправляется коэффициент, который может выходить за пределы одного разряда, потом эти коэффициенты преобразуются в нормальное двоичное число.
AMD использует метод Гольдшмита с итеративным вычислением 1/x и последующим домножением на делимое.

Проблемы деления


Теперь давайте разбираться, чем так плохо деление и почему отнимает много тактов. Основная проблема в том, что подбор результата надо делать, начиная с самого значащего бита, но потом происходит вычитание. А вычитание, как и сложение, делается, начиная с наименее значащего бита. И если попробовать сделать все вычисления за один такт, то длина цепочки однобитных сумматоров получится эквивалентной n^2, потому что сигнал будет постоянно наматывать круги от наименее значащего бита к наиболее значащему, чтобы в очередной раз сравнить и вычесть при необходимости. Посмотрим, как решается проблема в других алгоритмах.

Предыстория


На самом деле метод Гольдшмита, как и его сосед — метод Ньютона-Рафсона, вытекает из обычного деления столбиком. Подробно объяснять их не буду, в Википедии всё довольно понятно написано, здесь только нужные идеи из этих алгоритмов.

Базовая идея метода Ньютона


Сначала подбираем некое число, которое чуть меньше 1/y, потом перемножаем на x (он же делимое), получаем частное q, которое немного меньше необходимого. Перемножаем его на делитель и вычитаем из делимого. Получается остаток (пока всё как в обычном делении в столбик, только число обнуляемых разрядов за раз оказалось переменным). Потом остаток домножаем на тот же самый сомножитель, опять получаем некий кусочек частного, который прибавляем к результату и вычитаем из остатка, перемножив на делитель, и т.д.
На этом моменте, если подобрать коэффициент с точностью 8 бит, то можно перемножать на 8 бит, и у результата получать новые 8 бит за такт. Неплохой компромисс между скоростью и размером на кристалле.

Метод Ньютона


Если взять в качестве делимого 1.0 и посмотреть на процесс, не подставляя конкретное значение y, то после второй итерации получится, что из делимого вычитается делитель, умноженный на более точный коэффициент. Если для следующей итерации сразу взять этот более точный коэффициент (он же частное), то получится, что точность результата растёт намного быстрее, число верных битов удваивается, а не увеличивается на константу. На это число верных битов надо посмотреть внимательнее. Если в делителе число типа 1.0000xxx..., то ведущие нули и означают число верных битов в знаменателе. Поскольку в делимом единица, фактически процесс выглядит как подбор множителя, который превратит все биты произведения с делителем в 0 (ну или в 1, большой разницы нет, так как 1111 + 1 = 10000).
Всё бы хорошо, но для одной итерации приходится сначала перемножать остаток на 1/y, а потом результат на частное, что приводит к увеличению времени одной итерации.

Метод Гольдшмита


Развитие идеи состоит в том, что если уж надо добиться сплошных 1 в результате, то почему бы не делать это в знаменателе, умножая параллельно числитель и знаменатель на одно и то же число. В случае подбора 0 процесс выглядит совсем прозрачно.
100ab…
-__100ab… * a
-___100ab… * b
Очевидно, младшие разряды с остальными почти не пересекаются, поэтому для следующей итерации биты можно брать напрямую из результата (то есть y_next = y — y*000ab), разве что учесть, что из младших разрядов может прилететь один перенос.

Если подбирать 1, то получается немного неочевидно.
111ab…
+__111ab… * (1-a)
+___111ab… * (1-b)
Здесь мы как бы считаем, что 111 это 1 000 -1, поэтому сдвигаем добавку вправо на 1.

Новый алгоритм


Находить 1/x, подбирать знаменатель параллельно с числителем, заполнять единицами, записывать в разряды больше, чем там может поместиться, обрабатывать биты локально, так как они не пересекаются — всё это используем в новом алгоритме.
Раз быстрым методам можно начинать с обычного деления в столбик, то и мы начнём. Пусть у нас есть числитель 1.000… и знаменатель 0.1ххх…. Будем параллельно подбирать биты для изменения знаменателя и добавлять их к числителю. Идея состоит в том, что предыдущие значения складываются в момент вычисления каждого бита, и в результате длина цепочки битов в вычислениях максимум равна n.
__0.1xxx…
+_0.01xxx…
Если первый бит после 1 равен 0 (т.е. 0.10xx...), то добавляем к числителю бит, а к знаменателю x>>1. Теперь посмотрим на процесс после нескольких итераций.
__0.1xxx…
+…
+_____0.1[xxx.]…
+______0.[1xxx]…
Тут возникает проблема, потому что младшие биты при сложении могут дать перенос и не один. Для того чтобы не ошибиться, надо брать ширину проверяемого окна не 1 бит, а log(n). Но тогда каждый новый бит будет увеличивать цепочку на log(n), что в итоге даст n*log(n) до самого младшего бита результата, а это ничем не лучше обычного деления столбиком, если использовать ускоренные сумматоры. К тому же размер логики разрастётся и займёт место на чипе.

Избавляемся от переносов


Что вообще мешает считать быстро? Сложения в первую очередь, из-за них цепочка получается длинной. А во-вторых, из дебрей маленьких битов может прилететь перенос, который испортит результат. Обе проблемы можно исправить разом, если разрешить в каждом разряде хранить больше, чем там должно быть. Пусть у нас всё так же разряд означает (0 или 1) * 2^n, но вместо 0,1 там может быть 0,1,2,3. Старший бит в таком разряде означает перенос в следующий разряд, то есть мы как бы разрываем биты сумматора и прибавление переноса оставляем на следующий этап.
Фактически получится сумматор с разорванными переносами habr.com/ru/articles/712892.
Теперь модифицируем алгоритм, чтобы ничего не тормозило. На каждом этапе сначала освобождаем старший бит разряда, перенося его влево. В итоге получаем в разряде значение от 0 до 2. Потом к этому значению добавляем новое слагаемое.
Очевидно, если в какой-то момент образуется цепочка 1 1 1 2, то на следующем этапе она превратится в 1 1 2 0, то есть перенос понемногу двигает переполнение влево. В этот момент мы делаем нечто похожее на краш-тест автомобиля. Когда в обрабатываемый разряд делителя набивается значение 3, мы его замораживаем и переходим к следующему значению. Пока что длина цепочки обработки получается равной одному однобитному сумматору за счёт разорванного сумматора.
В процессе обработки знаменатель 0.1xxx… превращается в 0.1333..., а числитель 1.000… в 1.2313…. Каждый новый разряд результата добавляет в цепочку логики 1 однобитный сумматор и логику обработки старшего бита. Сейчас с ней разберёмся. Для того, чтобы получить в старшем разряде 3, надо добавить к нему знаменатель, возможно пару раз, возможно со сдвигом.
0.13331…
+___0.1xxx
Тут важно помнить, что сложение может делаться только после переноса, чтобы не возникло переполнения 3 + 1 = 0. Итоговая логика для старшего бита следующая:
  • если в разряде уже 3, то отправляем дальше без активации переноса
  • смотрим на сумму разряда и переноса
  • если 3, то добавляем 0
  • если 2, то добавляем 1 (это необязательная ветка, на первом этапе надо заполнить старший бит)
  • если 1, то 1 разряд
  • если 0, то 2 разряда (то есть x<<1)

на втором этапе
  • если сумма 3, то отправляем с активацией переноса
  • если 2, добавляем 1 разряд

То есть обработка одного старшего разряда занимает примерно пару сложений и мультиплексоров, то есть суммарная цепочка будет 2*n. На выходе получаем число в странной системе счисления, которое просто превратить в нормальное. Для этого старшие биты надо вынести в другую переменную и прибавить со сдвигом, ведь они фактически были битами переноса. Это действие добавит ещё немного времени обработки, но поскольку старшие разряды приходят раньше, фактически это время будет небольшим при наличии цепочек ускорения переноса в сумматоре.
В конце надо не забыть перемножить числитель на полученное значение 1/x.

Перспективы развития


Два этапа подбора разряда можно сократить до одного, приведя знаменатель к форме 0.10xxx. Если уж можно ускорить сложение, параллельно считая для старших разрядов сумму с переносом из младшего бита и без, то возможно и в этом алгоритме получится сделать нечто подобное. Выходное значение в выбранной системе счисления определено неоднозначно, что позволяет трамбовать 3-ки в поток до того как они окажутся в последнем разряде. Это может привести к тому, что можно будет перескакивать сразу по 2 разряда или больше. Натрамбовать можно попробовать в середину потока или в начало, правда есть проблема с тем, что сложение позволяет добавлять за один раз только по 1 и понадобится расширять диапазон значений разряда. Можно предварительно привести знаменатель к значению 0.1111xxx… и надеяться на появление дополнительных троек в предконечных битах. Можно не пропускать сложения: если нельзя прибавлять в текущий бит, прибавить в предыдущий. При большей разрядности одного разряда можно подбирать сразу несколько бит выходного разряда. Ещё можно заметить, что код для каждой строки одинаковый, поэтому хорошо нарезается и зацикливается, если захочется разбить на несколько стадий.

Умножение


Для умножения предложенный алгоритм может сработать ещё лучше. Если на каждом этапе добавляется по одному числу, то за счёт разрыва переноса результат можно получить за n однобитных сумматоров. Понятно, что там ещё можно добавить оптимизаций, нарезав цепочку и сложив быстрым сумматором.

Реализация


Для начала тест на Python.
overflow_1.py
# шаг сложения с переносом только на один разряд
def add_bit(accum, carry, value):
	carry = carry << 1
	new_carry = (accum & value) | (accum & carry) | (value & carry)
	new_accum = accum ^ value ^ carry
	return new_accum, new_carry

#эти числа ломали другие алгоритмы
#value = 0b1_0011_0010_0000 # на что делим
#value = 0b1_1000_0000_0000
#value = 0b1_1110_1111_1111
#value = 0b1_0111_1111_1111
#value = 0b1_0100_0000_0000
#value = 0b1_0101_1101_0101
#value = 0b1_0001_0001_0000
#value = 0b1_0000_0100_0000 #даёт 111 после запятой в результате
#value = 0b1_0000_0000_0001
value = 0b1_0000_0000_0000 # здесь точности не хватает, получается 111...
#value = 0b1_1111_1111_1111
rvalue = 0b1_0000_0000_0000_0000_0000_0000 // value # что должно получиться
print(bin(rvalue)[2:].rjust(16), " reverse")

divider = value
count = len(bin(divider)) - 2 # число значащих бит
res = 1 # результат расчётов
sum = divider # биты разрядов
carry = 0 # биты переноса, они же старшие биты разрядов
sig_bits = 2 # биты, учитываемые для расчётов

print("value      ", bin(value)[2:].rjust(count+1))
print("start_value", bin(sum)[2:].rjust(count+1))
for i in range(0, count - 1): # вычисление битов
	mask = (1 << sig_bits) - 1
	print("\ni = ", i, "mask = ", bin(mask)[2:])
	# выделяем место для нового разряда
	sum = (sum << 1) & ((1 << count) - 1)
	carry = (carry << 1) & ((1 << count) - 1)
	# смотрим на биты в двух старших разрядах
	sum_part = ((sum << (sig_bits)) >> (count+1))
	carry_part = ((carry << sig_bits) >> (count)) & mask
	digit = sum_part + (carry_part & 2) # значение в текущем разряде
	print("sum, carry, digit ", bin(sum_part)[2:], bin(carry_part)[2:], bin(digit)[2:])
	res = res << 1
	if digit != 3: # надо делать перенос
		digit = sum_part + carry_part # значение в разряде с учётом переноса
		delta = 0
		if digit == 0:
			delta = divider << 1
			res += 2
		elif digit == 1:
			delta = divider
			res += 1
		#print("add2 ", bin(sum)[2:].ljust(16), bin(carry)[2:].ljust(16), bin(delta)[2:].ljust(16))
		print("add2 s     ", bin(sum)[2:].rjust(count+1))
		print("add2 c     ", bin(carry)[2:].rjust(count+1))
		print("add2 d     ", bin(delta)[2:].rjust(count+1))
		sum, carry = add_bit(sum, carry, delta)
	
	sum_part = ((sum << sig_bits) >> (count + 1)) # здесь оба бита, так как при сдвиге добавки она попадает не в перенос
	carry_part = ((carry << sig_bits) >> (count)) & mask
	digit = sum_part + (carry_part & 2)
	print("sum, carry, digit ", bin(sum_part)[2:], bin(carry_part)[2:], bin(digit)[2:])
	if digit != 3: # надо делать перенос
		digit = sum_part + carry_part
		delta = 0
		if digit == 2:
			delta = divider
			res += 1
		#print("add1 ", bin(sum)[2:].ljust(16), bin(carry)[2:].ljust(16), bin(delta)[2:].ljust(16))
		print("add1 s     ", bin(sum)[2:].rjust(count+1))
		print("add1 c     ", bin(carry)[2:].rjust(count+1))
		print("add1 d     ", bin(delta)[2:].rjust(count+1))
		sum, carry = add_bit(sum, carry, delta)
	print("           ", bin(sum)[2:].rjust(count+1))
	print("           ", bin(carry)[2:].rjust(count+1))
	print("           ", bin(res)[2:].rjust(count+1))


print("sum:        ", bin((sum + (carry << 1))>>(count-3))[2:].ljust(16))
print("error:      ", bin(res - (rvalue << 6))[2:].ljust(16))

print("\n")
print("result:        ", bin(res)[2:].ljust(16), res)
print("reverse:       ", bin(rvalue)[2:].ljust(16), rvalue)
print("\n")
print("value:         ", bin(value)[2:].ljust(16), value)
print("value*res:     ", bin(value*res)[2:])
print("value*reverse: ", bin(value*rvalue)[2:])




В конце в знаменателе получается число 0.1333..., которое распадается в 0.1111… + 0.111… = 1.1101, немного не хватает до 10.0, то есть знаменатель чуть меньше чем надо, значит и числитель тоже. Просто округлять нельзя, так как числа вроде 10001000 могут давать в результате ряд 1 после запятой, поэтому в конце проверяем, надо ли прибавить к результату 1. Возможно для float деления подобный алгоритм подходит лучше.
Реализация на verilog выглядит примерно так (на практике не проверял).
float_div.v
module RiscVFastDivider
(
	input [31:0] x, //делимое в формате 1*
	output [31:0] result //результат работы в формате 1*. Когда делим 1/0.1, в результате 111...
);

// result = 1 / x
// x = [0.100..., 0.111...]
// 1/x = [1.000..., 1.111...]
//10.000 - это может быть (1 << 31) (левая граница больше правой)

always @* begin
	integer i;
	wire [31:0] numerator = 1;
	wire [31:0] numerator_carry = 0;
	wire [31:0] divider = x;
	wire [31:0] divider_sum = divider;
	wire [31:0] divider_carry = 0;      //это биты переноса делителя, т.е. бит = 1, когда разряд = 2 или 3
	
	// знаменатель с помощью умножений на разряды параллельно с числителем
	// приводим к виду 0.1333xxx == 0.1111xxx + 0.111xxx = 1.110xxx = 10.00 - 0.00xxx

	for(i = 0; i < 32; i = i + 1) begin
		// рассматриваем очередной разряд в знаменателе
		divider_sum = divider_sum << 1;
		divider_carry = divider_carry << 1;
		
		wire [1:0] out_place; // что запишем в разряд результата
		
		// итерация для установления старшего бита разряда в 1
		wire [1:0] place = {divider_carry[31], divider_sum[31]}; //берём очередной разряд делителя
		wire [31:0] divider_sum_2;  //после первого шага здесь должно быть минимум 2 в 31 разряде
		wire [31:0] divider_carry_2;
		if (place == 3) begin
			divider_sum_2 = divider_sum;
			divider_carry_2 = divider_carry;
			out_place = 0;
		end
		else begin
			wire carry_bit = divider_carry[30];
			wire [1:0] next_place = place + carry_bit;
			wire [31:0] addition;
			if (next_place == 3 or next_place == 2) begin
				addition = 0;
				out_place = 0;
			end
			else if (next_place == 1) begin
				addition = divider;
				out_place = 1;
			end
			else
				addition = divider << 2;
				out_place = 2;
			end
			//делаем сложение с распространением переноса только на 1 шаг
			//самый значащий бит из переноса попадает в последний бит значения
			//но поскольку он с прошлого раза точно =1, расширять сумму до 33 бит не будем
			//по той же причине addition = divider << 2 не расширяли
			wire [31:0] carry_addition = divider_carry << 1;
			divider_sum_2 = divider_sum ^ carry_addition ^ addition;
			divider_carry_2 = (divider_sum & carry_addition) | (divider_sum & addition) | (carry_addition & addition);
			
		end
		
		// теперь устанавливаем младший бит разряда в 1
		wire place_2 = divider_sum_2[31]; //берём очередной разряд делителя
		if (place_2 == 1) begin
			divider_sum = divider_sum_2;
			divider_carry = divider_carry_2;
			out_place = out_place + 0;
		end
		else begin
			wire carry_bit_2 = divider_carry_2[30];
			wire [31:0] addition_2;
			if (carry_bit_2 == 1) begin
				addition_2 = 0;
				out_place = out_place + 0;
			end
			else begin
				addition_2 = divider;
				out_place = out_place + 1;
			end
			
			// ещё раз продвигаем перенос
			wire [31:0] carry_addition_2 = divider_carry_2 << 1;
			divider_sum = divider_sum_2 ^ carry_addition_2 ^ addition_2;
			divider_carry = (divider_sum_2 & carry_addition_2) | (divider_sum_2 & addition_2) | (carry_addition_2 & addition_2);
		end
		
		// добавляем очередной разряд в числитель
		numerator = (numerator << 1);
		numerator_carry = (numerator_carry << 1);
		numerator[0] = out_place[0];
		numerator_carry[0] = out_place[1];
	end
	
	result = (numerator + (numerator_carry << 1)) >> 1;
end


Версия без умножения в конце


Вроде бы всё работает, но алгоритм не выдаёт остаток, а хотелось бы. Раз уж получилось с единицами, почему бы не попробовать с нулями. Для этого возьмём обычный алгоритм деления и ускорим его за счёт разорванных переносов. На этот раз в разряде надо получать 0, поэтому можно не волноваться насчёт переполнения и убрать подобную проверку. С другой стороны, вычитание выглядит как сложение, но надо не забывать, какой бит сейчас считается битом знака. И так, берём некий остаток 1xxx и вычитаем из него делитель 1yyy. При вычитании стараемся протаскивать 1 в числителе, оставляя её на 1 бит левее рассматриваемого, то есть вычитаем не так
1xxx —
1yyy
а так
1xxx —
_1yyy

Это надо для того, чтобы гарантированно не получить отрицательного остатка. С другой стороны, подобный фокус станет очевидным, если посмотреть на отрицательное число.
-0_1yyy = 1_yyyy
Старший разряд превращается в разряд знака. И когда мы такое число прибавляем к числителю, знак укатывается влево
0000_1xxx +
1111_1yyy_y =
0000_?zzz_z
? означает, что с первого раза может не получиться, поэтому здесь тоже две итерации на разряд. Собираем итоговый алгоритм.
Берём 0 или 1 из разряда переполнения (её мы протягиваем по всей цепочке), добавляем текущий разряд и добавляем перенос из более младшего разряда, получаем 10 + 11 + 1 = 110, т.е. максимум надо скомпенсировать 6 в текущем разряде.
На первой итерации стараемся обнулить старший бит.
  • если сумма 1, вычитаем 0
  • если 2 или 3, вычитаем 1
  • если 4+, вычитаем 2

Тут в качестве оптимизации можно попробовать не учитывать младший разряд.
На второй итерации обнуляем второй бит.
  • если сумма 0 или 1, вычитаем 0
  • если 2 или 3, вычитаем 1

Обычно -1yyy = 11yy, но есть предельный случай -1000 = 1000, подстановкой обоих вариантов можно убедиться, что в итоге в разряде останется 0 или 1, то есть алгоритм сработает как надо.

Для начала проверим алгоритм на Python.
overflow_0.py
#прямое деление столбиком

def bin2(value):
	return "{:_b}".format(value)

# шаг сложения с переносом только на один разряд
# но теперь 11 интерпретируем как -1
def add_bit(accum, carry, value):
	carry = carry << 1
	new_carry = (accum & value) | (accum & carry) | (value & carry)
	new_accum = accum ^ value ^ carry
	return new_accum, new_carry

#эти числа ломали другие алгоритмы
#value = 0b1_0011_0010_0000 # на что делим
#value = 0b1_1000_0000_0000
#value = 0b1_1110_1111_1111
#value = 0b1_0111_1111_1111
#value = 0b1_0100_0000_0000
#value = 0b1_0101_1101_0101
#value = 0b1_0001_0001_0000
#value = 0b1_0000_0100_0000 #даёт 111 после запятой в результате
value = 0b1_0000_0000_0001
#value = 0b1_0000_0000_0000 # здесь точности не хватает, получается 111...
#value = 0b1_1111_1111_1111
divisible = 0b1_0000_0000_0000_0000_0000_0000
rvalue = divisible // value # что должно получиться

divider = value
count = len(bin(divider)) - 2 # число значащих бит
div_count = len(bin(divisible)) - 2
res = 0 # результат расчётов
remainder = 0 # остаток от деления
carry = 0 # биты переноса, они же старшие биты разрядов остатка
sig_bits = 2 # биты, учитываемые для расчётов
mask = (1 << count) - 1
wide_mask = (1 << (count + 1)) - 1

print("reverse    ", bin2(rvalue).rjust(count+5))
print("value      ", bin2(value & wide_mask).rjust(count+5))
print("wide_mask  ", bin2(wide_mask).rjust(count+5))
print("-value     ", bin2((-value) & wide_mask).rjust(count+5))
print("-value*2   ", bin2(((-value) & wide_mask)<<1).rjust(count+5))
for i in range(0, div_count): # вычисление битов
	print("\ni = ", i)
	# вдвигаем новый разряд остатка
	remainder = ((remainder << 1) + ((divisible >> (div_count - i - 1)) & 1)) & wide_mask # здесь храним дополнительный бит остатка
	carry = (carry << 1) & mask
	
	# смотрим на биты в двух старших разрядах
	sum_part = remainder >> (count - sig_bits + 1)
	carry_part = carry >> (count - sig_bits)
	digit = sum_part + (carry_part & 2) # значение в текущем разряде
	print("remainder_d, carry, digit ", bin2(sum_part), bin2(carry_part), bin2(digit))
	res = res << 1
	if digit != 0 or digit == 0: # надо делать перенос
		digit = sum_part + carry_part # значение в разряде с учётом переноса
		delta = 0
		if digit == 2 or digit == 3:
			delta = (-divider) & wide_mask
			res += 1
		if digit & 4:
			delta = ((-divider) << 1) & wide_mask
			res += 2
		print("add2 r     ", bin2(remainder).rjust(count+4))
		print("add2 c     ", bin2(carry).rjust(count+4))
		print("add2 d     ", bin2(delta).rjust(count+4))
		remainder, carry = add_bit(remainder, carry, delta)
		carry = carry & mask
	
	sum_part = remainder >> (count - sig_bits + 1)
	carry_part = carry >> (count - sig_bits)
	digit = sum_part + (carry_part & 2)
	print("remainder_d, carry, digit ", bin2(sum_part), bin2(carry_part), bin2(digit))
	if digit != 0 or digit == 0: # надо делать перенос
		digit = sum_part + carry_part
		delta = 0
		if digit & 2:
			delta = (-divider) & wide_mask
			res += 1
		print("add1 r     ", bin2(remainder).rjust(count+4))
		print("add1 c     ", bin2(carry).rjust(count+4))
		print("add1 d     ", bin2(delta).rjust(count+4))
		remainder, carry = add_bit(remainder, carry, delta)
		remainder = remainder & (wide_mask >> 1) # отбрасываем обработанный разряд
		carry = carry & (mask >> 1)
	print("          ", bin2(remainder).rjust(count+5))
	print("          ", bin2(carry).rjust(count+5))
	print("          ", bin2(remainder + (carry << 1)).rjust(count+5))
	print("          ", bin2(res).rjust(count+5))

# перенос может дать максимум ещё +1 в старший разряд
# здесь сравниваем два разряда, чтобы не пропустить 1
sum_part = remainder >> (count - sig_bits)
carry_part = carry >> (count - sig_bits - 1)
digit = sum_part + carry_part
digit_div = divider >> (count - sig_bits)
print("\n")
print("end digit ", digit, digit_div)
if digit > digit_div:
	delta = (-divider) & wide_mask
	res += 1
	remainder, carry = add_bit(remainder, carry, delta)
	print("-1 by digit")
	print("          ", bin2(remainder).rjust(count+5))
	print("          ", bin2(carry).rjust(count+5))
	print("          ", bin2(remainder + (carry << 1)).rjust(count+5))
	print("          ", bin2(res).rjust(count+5))
	print("\n")

# результат может отставать на 1 бит
result_remainder = (remainder + (carry << 1)) & wide_mask
if result_remainder >= divider:
	result_remainder -= divider
	res += 1
	print("-1 end round")

real_remainder = divisible % value
print("rem:        ", bin2(result_remainder).ljust(16), result_remainder)
print("rem_real:   ", bin2(real_remainder).ljust(16), real_remainder)
print("\n")
print("result:        ", bin2(res).ljust(16), res)
print("reverse:       ", bin2(rvalue).ljust(16), rvalue)
print("\n")
print("value:         ", bin2(value).ljust(16), value)
print("value*result:  ", bin2(value*res))
print("value*reverse: ", bin2(value*rvalue))


Вроде бы работает. Теперь собираем на verilog. Сложение с переносами только на 1 бит выглядит так.
reg [32:0] remainder_bit;
reg [31:0] remainder_carry;

reg [32:0] remainder_bit_2;
reg [31:0] remainder_carry_2;

reg [32:0] addition;

carry_addition = remainder_carry << 1;
remainder_bit_2 = remainder_bit ^ carry_addition ^ addition;
remainder_carry_2 = (remainder_bit & carry_addition) | (remainder_bit & addition) | (carry_addition & addition);


Возможно, такая ручная запись сложения приводит к тому, что компилятор ПЛИС не может его ускорить.

Обработка старшего бита для одного разряда.
next_place = remainder_bit[32:31] + remainder_carry[31:30];
reg [1:0] out_place; // что запишем в разряд частного
reg [32:0] addition;
if (next_place[2]) begin
	addition = (-divider) << 1; //1[xx]
	out_place = 2;
end
else if (next_place[1]) begin
	addition = -divider; //1[1x]
	out_place = 1;
end
else begin
	addition = 0;
	out_place = 0;
end

После работы алгоритма в младшем разряде может остаться бит переполнения и для этого надо будет 1 раз вычесть делитель.
correction = 0;
end_digit = remainder_bit[32:30] + remainder_carry[31:29];
if (end_digit > divider[31:30]) begin
	correction = 1;
	...
end

Ещё после преобразования остатка к нормальному виду может оказаться, что в нём остался один делитель.
remainder_sum = remainder_bit + (remainder_carry << 1);
remainder_diff = remainder_sum - divider;
if ($signed(remainder_diff) >= 0) begin
	correction = correction + 1;
	remainder_sum = remainder_diff;
end

Перед запуском алгоритма надо выравнять делитель по левому краю. Для этого рекурсивно выбираем половину, в которой есть ненулевой разряд и считаем сдвиг.
function [4:0] clz(input [31:0] value);
reg [4:0] result;
reg [15:0] v16;
reg [7:0] v8;
reg [3:0] v4;
reg [1:0] v2;
begin
	result[4] = value[31:16] == 0? 1 : 0;
	v16 = result[4]? value[15:0] : value[31:16];
	
	result[3] = v16[15:8] == 0? 1 : 0;
	v8 = result[3]? v16[7:0] : v16[15:8];
	
	result[2] = v8[7:4] == 0? 1 : 0;
	v4 = result[2]? v8[3:0] : v8[7:4];
	
	result[1] = v4[3:2] == 0? 1 : 0;
	v2 = result[1]? v4[1:0] : v4[3:2];
	
	result[0] = v2[1] == 0? 1 : 0;
	clz = result;
end
endfunction

Теперь собираем всё в модуль.
fast_div.v
module RiscVFastDiv
(
	input [31:0] x, //на входе и на выходе неотрицательные числа
	input [31:0] y,
	output [31:0] quotient,  // частное
	output [31:0] remainder  // остаток
);

wire [31:0] divisible;    // делимое, смещённое на столько же разрядов что и делитель
wire [31:0] divisible2;   // та часть делимого, которая вылезла за границы при смещении
wire [31:0] divider;      // делитель с 1 в старшем разряде
reg [31:0] quotient_out; // частное
reg [31:0] remainder_out;// остаток

// [   divisible2] [divisible]
// [divider      ] [000000000]

// алгоритм основан на том факте, что 100 - 10 = 10, в свою очередь 10 - 1 = 1, ну а 1 как-нибудь можно обнулить
reg [31:0] quotient_bit;
reg [31:0] quotient_carry;
reg [32:0] remainder_bit;
reg [31:0] remainder_carry;
reg [31:0] divisible_bit;
reg [1:0] out_place;

reg [2:0] next_place;
reg [32:0] addition;
reg [32:0] carry_addition;
reg [32:0] remainder_bit_2;
reg [31:0] remainder_carry_2;

reg [1:0] next_place_2;
reg [32:0] addition_2;
reg [32:0] carry_addition_2;

reg [1:0] correction;

reg [2:0] end_digit;
reg [32:0] addition_3;
reg [32:0] carry_addition_3;
reg [32:0] remainder_bit_3;

reg [32:0] remainder_sum;
reg [32:0] remainder_diff;

always @(*)
begin
	quotient_bit = 0; // в частном на старте пусто
	quotient_carry = 0;
	remainder_bit = divisible2; //эти биты остатка автоматически задвинуты на старте и точно меньше делителя
	remainder_carry = 0;      //это биты переноса остатка, т.е. бит = 1, когда разряд = 2 или 3
	divisible_bit = divisible;

	repeat(32) begin
		// вдвигаем очередной бит делимого в остаток
		remainder_bit = remainder_bit << 1;
		remainder_carry = remainder_carry << 1;
		remainder_bit[0] = divisible_bit[31];
		divisible_bit = divisible_bit << 1;
		
		//reg [1:0] out_place; // что запишем в разряд частного
		
		// итерация для установления старшего бита (2) разряда в 0
		//берём очередной разряд делителя с учётом остатка с прошлого раза в remainder_bit[32]
		next_place = remainder_bit[32:31] + remainder_carry[31:30];
		//reg [32:0] addition;
		if (next_place[2]) begin
			addition = (-divider) << 1; //1[xx]
			out_place = 2;
		end
		else if (next_place[1]) begin
			addition = -divider; //1[1x]
			out_place = 1;
		end
		else begin
			addition = 0;
			out_place = 0;
		end
		//делаем сложение с распространением переноса только на 1 шаг
		carry_addition = remainder_carry << 1;
		remainder_bit_2 = remainder_bit ^ carry_addition ^ addition;
		remainder_carry_2 = (remainder_bit & carry_addition) | (remainder_bit & addition) | (carry_addition & addition);
		
		// теперь устанавливаем младший бит (1) разряда в 0
		next_place_2 = remainder_bit_2[32:31] + remainder_carry_2[31:30];
		//reg [32:0] addition_2;
		if (next_place_2[1]) begin
			addition_2 = -divider;  // [1x]
			out_place = out_place + 1;
		end
		else begin
			addition_2 = 0;
			out_place = out_place + 0;
		end
		
		// ещё раз продвигаем перенос
		// в старшем бите(32) должно получиться 0
		carry_addition_2 = remainder_carry_2 << 1;
		remainder_bit = remainder_bit_2 ^ carry_addition_2 ^ addition_2;
		remainder_carry = (remainder_bit_2 & carry_addition_2) | (remainder_bit_2 & addition_2) | (carry_addition_2 & addition_2);
		
		// добавляем очередной разряд в частное
		quotient_bit = (quotient_bit << 1);
		quotient_carry = (quotient_carry << 1);
		quotient_bit[0] = out_place[0];
		quotient_carry[0] = out_place[1];
	end
	
	correction = 0;
	//теперь делаем ещё одно продвижение переноса
	//так как в худшем случае переносы могут добавить 1 бит остатка
	//и для этого смотрим 2 последних разряда
	end_digit = remainder_bit[32:30] + remainder_carry[31:29];
	if (end_digit > divider[31:30]) begin
		correction = 1;
		addition_3 = -divider;
		carry_addition_3 = remainder_carry << 1;
		remainder_bit_3 = remainder_bit;
		remainder_bit = remainder_bit_3 ^ carry_addition_3 ^ addition_3;
		remainder_carry = (remainder_bit_3 & carry_addition_3) | (remainder_bit_3 & addition_3) | (carry_addition_3 & addition_3);
	end
	
	// в конце складываем и при необходимости округляем на 1
	remainder_sum = remainder_bit + (remainder_carry << 1);
	remainder_diff = remainder_sum - divider;
	if ($signed(remainder_diff) >= 0) begin
		correction = correction + 1;
		remainder_sum = remainder_diff;
	end
	
	// выдаём результат
	remainder_out = remainder_sum;
	quotient_out = quotient_bit + (quotient_carry << 1) + correction;
end

function [4:0] clz(input [31:0] value);
reg [4:0] result;
reg [15:0] v16;
reg [7:0] v8;
reg [3:0] v4;
reg [1:0] v2;
begin
	result[4] = value[31:16] == 0? 1 : 0;
	v16 = result[4]? value[15:0] : value[31:16];
	
	result[3] = v16[15:8] == 0? 1 : 0;
	v8 = result[3]? v16[7:0] : v16[15:8];
	
	result[2] = v8[7:4] == 0? 1 : 0;
	v4 = result[2]? v8[3:0] : v8[7:4];
	
	result[1] = v4[3:2] == 0? 1 : 0;
	v2 = result[1]? v4[1:0] : v4[3:2];
	
	result[0] = v2[1] == 0? 1 : 0;
	clz = result;
end
endfunction

//теперь присоединяем входы и выходы к блоку деления
wire [4:0] offset = clz(y);
assign divider = y << offset;
assign {divisible2, divisible} = x << offset;
assign remainder = remainder_out >> offset;
assign quotient = quotient_out;

endmodule


Внедрение


Вернёмся к исходной цели статьи — сборке RISC-V процессора. Умножение относится к отдельному расширению M. Чтобы компилятор начал его использовать, зададим правильную архитектуру в параметрах компиляции.
set arch=rv32em

Библиотечные функции __umulsi3 и прочие можно удалить (ну или оставить на случай тестов без умножения).
Модуль будет в двух исполнениях, для минимальной сборки на ПЛИС и некий идеальный вариант для ASIC. Минимальный вариант будет обычным побитовым, поэтому делаем небольшой шажок к конвейеру и добавляем флаг ожидания текущей инструкции.
wire is_wait; //на текущем такте инструкция ещё не готова

Изменение регистра указателя на код pc и регистра результата rd теперь делается только в момент, когда команда отработала.
if (!is_wait) begin
    regs[op_rd] <= rd_result;
    pc <= pc_result;
end

На этом добавление поддержки многотактовых команд закончено, дальше опять идём смотреть в мануалы. Команды умножения идут под тем же кодом, что и другие команды alu.





Раз опкод одинаковый, есть смысл вынести команды вычисления в отдельный модуль alu.v. В коде ядра это будет выглядеть примерно так.
wire [31:0] rd_alu;
wire is_alu_wait;
RiscVAlu alu(
                .clock(clock),
                .reset(reset),
                .is_op_alu(is_op_alu),
                .is_op_alu_imm(is_op_alu_imm),
                .op_funct3(op_funct3),
                .op_funct7(op_funct7),
                .reg_s1(reg_s1),
                .reg_s2(reg_s2),
                .imm(op_immediate_i),
                .rd_alu(rd_alu),
                .is_alu_wait(is_alu_wait)
            );

Пока модуль умножения единственный, который выполняет команды несколько тактов, так что флаг задержки берём прямо из него.
assign is_wait = is_alu_wait;

Теперь посмотрим, что у нас будет в новом модуле. Во-первых, надо не ломать старый функционал.
//обработка (add, sub, xor, or, and, sll, srl, sra, slt, sltu)
//в случае лёгких инструкций вычисляем результат сразу
wire [31:0] alu_operand2 = is_op_alu ? reg_s2 : is_op_alu_imm ? imm : 0;
wire [31:0] rd_alu1 = op_funct3_a == 3'd0 ? (is_op_alu && op_funct7[5] ? reg_s1 - alu_operand2 : reg_s1 + alu_operand2) :
					  op_funct3_a == 3'd4 ? reg_s1 ^ alu_operand2 :
					  op_funct3_a == 3'd6 ? reg_s1 | alu_operand2 :
					  op_funct3_a == 3'd7 ? reg_s1 & alu_operand2 :
					  op_funct3_a == 3'd1 ? reg_s1 << alu_operand2[4:0] :
					  op_funct3_a == 3'd5 ? (op_funct7[5] ? $signed(reg_s1) >>> alu_operand2[4:0] : reg_s1 >> alu_operand2[4:0]) :
					  op_funct3_a == 3'd2 ? $signed(reg_s1) < $signed(alu_operand2) :
					  op_funct3_a == 3'd3 ? reg_s1 < alu_operand2 : //TODO для больших imm проверить
					  0; //невозможный результат

Во-вторых, ура, добрались до умножения. Выбирать, было ли это умножение или простая инструкция, надо по флагу op_funct7.
wire is_op_muldiv = is_op_alu && op_funct7[0];
wire [2:0] op_funct3 = is_op_muldiv ? op_funct3_in : 3'b0; //отдельная стабилизация для модуля M
wire is_op_multiply = !op_funct3[2];
wire is_op_mul_signed = !op_funct3[1]; //mul, mulh
//wire is_op_mul_low = op_funct3[1:0] == 0; //mul
wire is_op_mul_extend_sign = op_funct3[1:0] == 2; //mulhsu //умножаем как беззнаковые, но сам знак надо расширить до 64 бит
wire is_op_div_signed = !op_funct3[0]; //div, rem
//wire is_op_remainder = op_funct3[2]; //rem, remu

Для меньших 32 бит компилятор всегда создаёт команду mul, независимо от типов аргументов. Чтобы уговорить его вызвать остальные инструкции, надо преобразовать аргумент в 64-битные и взять старшие 32 бита. Вообще в мануале советуют вызывать умножение сначала для старших 32 бит и кэшировать результат, чтобы младшие 32 бита получить быстро, но у нас кэша не будет. Так вот, поскольку инструкция для младших бит не зависит от типа аргументов, теоретически её можно всегда делать как для беззнаковых, но практически числа обычно небольшие, поэтому лучше избавиться от лишних единиц в старших битах и взять модуль числа. Кроме того, это надо для деления.
wire need_restore_sign = is_op_multiply ? is_op_mul_signed : is_op_div_signed;
wire start_muldiv_sign = need_restore_sign ? reg_s1[31] ^ reg_s2[31] : 1'b0; // = sign(x) * sign(y)
wire rem_sign_start = need_restore_sign ? reg_s1[31] : 1'b0; // = sign(x)
wire [31:0] start_x = (need_restore_sign && $signed(reg_s1) < 0) ? -reg_s1 : reg_s1;
wire [31:0] start_y = (need_restore_sign && $signed(reg_s2) < 0) ? -reg_s2 : reg_s2;

В случае многотактового деления и умножения понадобятся регистры, для их экономии мультиплексируем входы и выходы. Надо будет как-то определять, что инструкция уже вычисляется и не надо затирать всё стартовыми значениями. Ещё на всякий случай можно сохранить флаги, которые понадобятся для вычисления знака на выходе. Сейчас значения на входе модуля стабильны, но вдруг наступит гипертрединг )).
//инструкция запоминается снаружи, её надо сохранять не только для этого блока
//регистры используем для обоих операций
reg [31:0] x, y, r1, r2, r3;
//после начала работы не ориентируемся на входные значения
reg in_progress;
//считаем для беззнаковых, знак меняем в конце
reg muldiv_sign;
//для остатка от деления знак берётся из делимого
reg rem_sign;

Ещё надо учесть, что если хоть где-то ноль, то регистры не понадобятся.
wire need_wait = is_op_muldiv && reg_s1 && reg_s2;

Теперь непосредственно к алгоритмам. Тут было бы желательно совместить регистры так, чтобы хотя бы где-то совпала инструкция сдвига, потому что она тут повсюду, но как получится.
Для умножения один множитель двигаем влево и умножаем добавку на его нулевой бит, второй множитель двигаем вправо и используем в качестве добавки.
Расположение в регистрах: {r3, r2} = {r1 = sign, x} * y
wire [31:0] next_mul_y = (y >> 1); //поразрядное умножение
wire [63:0] current_mul_x = {r1, x}; //берём текущий x
wire [63:0] next_mul_x = current_mul_x << 1; //сдвигаем
wire [63:0] next_mul_val = {r3, r2} + (y[0] ? current_mul_x : 0); //и прибавляем к результату
wire mul_end = next_mul_y == 0; //условие окончания умножения

Для деления вдвигаем биты делимого, пока не закончатся. Для этого на старте выставляем текущий бит в одном из регистров, потом двигаем влево весь регистр и проверяем побитовое «И» между этим «итератором» и делимым.
Расположение в регистрах: {r3=q, r2=rem} = x / y, r1=msb (msb — most significant bit, он же итератор)
На старте для оптимизации можно пропустить лишние нули.
wire [31:0] start_msb = start_x[31:24] != 8'b0 ? (1 << 31) : //легковесная подгонка начального счётчика цикла
						start_x[23:16] != 8'b0 ? (1 << 23) :
						start_x[15:8] != 8'b0 ? (1 << 15) :
						(1 << 7);

//деление в столбик
//y (делитель) не меняем
//x (делимое) не меняем
wire [31:0] current_msb = r1;
wire [31:0] current_rem = r2;
wire [31:0] current_div_val = r3;
wire [31:0] next_msb = (current_msb >> 1); // выбираем следующий разряд делимого
wire [31:0] next_rem_tmp = {current_rem[30:0], current_msb & x ? 1'b1 : 1'b0}; // приписываем к остатку
//если остаток больше или равен (то есть делится на делитель), то вычитаем
//конкретную цифру подбирать не надо, так как двоичная система
wire [31:0] div_remainder_delta = next_rem_tmp - y;
wire [31:0] next_rem_val = $signed(div_remainder_delta) >= 0 ? div_remainder_delta : next_rem_tmp;
wire [31:0] next_div_val = {current_div_val[30:0], ~div_remainder_delta[31]}; //приписываем разряд к частному
wire div_end = next_msb == 0;

Алоритмы деления и умножения есть, теперь их выходы надо скомбинировать, чтобы записать в регистры.
wire [31:0] start_r1 = !is_op_multiply ? start_msb :
						is_op_mul_extend_sign ? (reg_s1[31] ? -1 : 0) :
						0;

wire [31:0] next_x = is_op_multiply ? next_mul_x[31:0] : x;
wire [31:0] next_y = is_op_multiply ? next_mul_y : y;
wire [31:0] next_r1 = is_op_multiply ? next_mul_x[63:32] : next_msb;
wire [31:0] next_r2 = is_op_multiply ? next_mul_val[31:0] : next_rem_val;
wire [31:0] next_r3 = is_op_multiply ? next_mul_val[63:32] : next_div_val;
wire divmul_end = is_op_multiply ? mul_end : div_end; //блок умножения закончил вычислять
wire next_in_progress = in_progress && !divmul_end; //когда законил, выключаем регистр активности блока

После того как алоритмы отработают, надо выдать конечные значения.
wire [63:0] mul_result = muldiv_sign ? -next_mul_val : next_mul_val;
wire [31:0] div_result = muldiv_sign ? -next_div_val : next_div_val;
wire [31:0] rem_result = rem_sign ? -next_rem_val : next_rem_val;
wire [31:0] rd_mul = !in_progress ? 0 : //на нулевом такте всё равно может быть только ноль
					!divmul_end ? 0 : //пока процесс идёт, не качаем затворы
					op_funct3 == 3'd0 ? mul_result[31:0] : //mul
					op_funct3 == 3'd1 ? mul_result[63:32] : //mulh
					op_funct3 == 3'd2 ? mul_result[63:32] : //mulsu
					op_funct3 == 3'd3 ? mul_result[63:32] : //mulu
					op_funct3 == 3'd4 ? div_result : //div
					op_funct3 == 3'd5 ? div_result : //divu
					op_funct3 == 3'd6 ? rem_result : //rem
					/*op_funct3 == 7 ?*/ rem_result;  //remu

assign is_alu_wait = !in_progress ? need_wait : !divmul_end;

Все расчёты проведены, осталось записать в регистры промежуточные значения.
wire divmul_active = need_wait || in_progress;
always@(posedge clock or posedge reset)
begin
	if (reset == 1) begin
		x <= 0;
		y <= 0;
		r1 <= 0;
		r2 <= 0;
		r3 <= 0;
		in_progress <= 0;
	end
	else if (divmul_active) begin
		if (!in_progress) begin // на нулевом такте просто запоминаем значения
			in_progress <= 1'b1;
			x <= start_x;
			y <= start_y;
			r1 <= start_r1; //расширение первого сомножителя знаком до 64 бит или счётчик цикла
			r2 <= 0; //младшие биты произведения или остаток
			r3 <= 0; //старшие биты произведения или частное
			muldiv_sign <= start_muldiv_sign; //надо ли в конце сменить знак
			rem_sign <= rem_sign_start;
		end
		else begin
			in_progress = next_in_progress;
			x <= next_x;
			y <= next_y;
			r1 <= next_r1;
			r2 <= next_r2;
			r3 <= next_r3;
		end
	end
end

//выдаём наружу результат
assign rd_alu = is_op_muldiv ? rd_mul :
					rd_alu1;

Файлы целиком
core.v
// ядро risc-v процессора

// базовый набор инструкций rv32i
`define opcode_load        7'b00000_11 //I //l**   rd,  rs1,imm     rd = m[rs1 + imm]; load bytes
`define opcode_store       7'b01000_11 //S //s**   rs1, rs2,imm     m[rs1 + imm] = rs2; store bytes
`define opcode_alu         7'b01100_11 //R //***   rd, rs1, rs2     rd = rs1 x rs2; arithmetical
`define opcode_alu_imm     7'b00100_11 //I //***   rd, rs1, imm     rd = rs1 x imm; arithmetical with immediate
`define opcode_load_upper  7'b01101_11 //U //lui   rd, imm          rd = imm << 12; load upper imm
`define opcode_add_upper   7'b00101_11 //U //auipc rd, imm          rd = pc + (imm << 12); add upper imm to PC
`define opcode_branch      7'b11000_11 //B //b**   rs1, rs2, imm    if (rs1 x rs2) pc += imm
`define opcode_jal         7'b11011_11 //J //jal   rd,imm   jump and link, rd = PC+4; PC += imm
`define opcode_jalr        7'b11001_11 //I //jalr  rd,rs1,imm   jump and link reg, rd = PC+4; PC = rs1 + imm

`ifdef __RV32E__
    `define REG_COUNT 16 //для embedded число регистров меньше
`else
    `define REG_COUNT 32
`endif

`include "alu.v"

module RiscVCore
(
	input clock,
	input reset,
	input irq,
	
	output [31:0] instruction_address,
	input  [31:0] instruction_data,
	
	output [31:0] data_address,
	output [1:0]  data_width,
	input  [31:0] data_in,
	output [31:0] data_out,
	output        data_read,
	output        data_write
);

//базовый набор регистров
reg [31:0] regs [0:`REG_COUNT-1]; //x0-x31
reg [31:0] pc;
wire is_wait; //на текущем такте инструкция ещё не готова

//достаём из pc адрес инструкции и посылаем в шину
assign instruction_address = pc;

//получаем из шины инструкцию
wire [31:0] instruction = instruction_data;

//расшифровываем код инструкции
wire[6:0] op_code = instruction[6:0]; //код операции
wire[4:0] op_rd = instruction[11:7]; //выходной регистр
wire[2:0] op_funct3 = instruction[14:12]; //подкод операции
wire[4:0] op_rs1 = instruction[19:15]; //регистр операнд 1
wire[4:0] op_rs2 = instruction[24:20]; //регистр операнд 2
wire[6:0] op_funct7 = instruction[31:25];
wire[31:0] op_immediate_i = {{20{instruction[31]}}, instruction[31:20]}; //встроенные данные инструкции I-типа
wire[31:0] op_immediate_s = {{20{instruction[31]}}, instruction[31:25], instruction[11:7]}; //встроенные данные инструкции S-типа
wire[31:0] op_immediate_u = {instruction[31:12], 12'b0};
wire[31:0] op_immediate_b = {{20{instruction[31]}}, instruction[7], 
                             instruction[30:25], instruction[11:8], 1'b0};
wire[31:0] op_immediate_j = {{12{instruction[31]}}, instruction[19:12], 
                             instruction[20], instruction[30:21], 1'b0};

//выбираем сработавшую инструкцию
wire is_op_load = op_code == `opcode_load;
wire is_op_store = op_code == `opcode_store;
wire is_op_alu = op_code == `opcode_alu;
wire is_op_alu_imm = op_code == `opcode_alu_imm;
wire is_op_load_upper = op_code == `opcode_load_upper;
wire is_op_add_upper = op_code == `opcode_add_upper;
wire is_op_branch = op_code == `opcode_branch;
wire is_op_jal = op_code == `opcode_jal;
wire is_op_jalr = op_code == `opcode_jalr;

wire error_opcode = !(is_op_load || is_op_store ||
                    is_op_alu || is_op_alu_imm ||
                    is_op_load_upper || is_op_add_upper ||
                    is_op_branch || is_op_jal || is_op_jalr);

//какой формат у инструкции
wire type_r = is_op_alu;
wire type_i = is_op_alu_imm || is_op_load || is_op_jalr;
wire type_s = is_op_store;
wire type_b = is_op_branch;
wire type_u = is_op_load_upper || is_op_add_upper;
wire type_j = is_op_jal;

//мультиплексируем константы
wire [31:0] immediate = type_i ? op_immediate_i :
				type_s ? op_immediate_s :
				type_b ? op_immediate_b :
				type_j ? op_immediate_j :
				type_u ? op_immediate_u :
				0;

//получаем регистры из адресов
//wire [31:0] reg_d = regs[op_rd];
wire [31:0] reg_s1 = regs[op_rs1];
wire [31:0] reg_s2 = regs[op_rs2];
wire signed [31:0] reg_s1_signed = reg_s1;
wire signed [31:0] reg_s2_signed = reg_s2;


//чтение памяти (lb, lh, lw, lbu, lhu), I-тип
assign data_read = is_op_load;
wire load_signed = ~op_funct3[2];
wire [31:0] rd_load = op_funct3[1:0] == 0 ? {{24{load_signed & data_in[7]}}, data_in[7:0]} : //0-byte
                      op_funct3[1:0] == 1 ? {{16{load_signed & data_in[15]}}, data_in[15:0]} : //1-half
                      data_in; //2-word

//запись памяти (sb, sh, sw), S-тип
assign data_write = is_op_store;
assign data_out = is_op_store ? reg_s2 : 0;

//общее для чтения и записи
assign data_address = (is_op_load || is_op_store) ? reg_s1 + immediate : 0;
assign data_width = (is_op_load || is_op_store) ? op_funct3[1:0] : 'b11; //0-byte, 1-half, 2-word

//обработка арифметических операций
//(add, sub, xor, or, and, sll, srl, sra, slt, sltu)
//(mul, mulh, mulsu, mulu, div, divu, rem, remu)
wire [31:0] rd_alu;
wire is_alu_wait;
RiscVAlu alu(
				.clock(clock),
				.reset(reset),
				.is_op_alu(is_op_alu),
				.is_op_alu_imm(is_op_alu_imm),
				.op_funct3_in(op_funct3),
				.op_funct7(op_funct7),
				.reg_s1(reg_s1),
				.reg_s2(reg_s2),
				.imm(immediate),
				.rd_alu(rd_alu),
				.is_alu_wait(is_alu_wait)
			);

//обработка upper immediate
wire [31:0] rd_load_upper = immediate; //lui
wire [31:0] rd_add_upper = pc + immediate; //auipc

//обработка ветвлений
wire [31:0] pc_branch = pc + immediate;
wire branch_fired = op_funct3 == 0 && reg_s1 == reg_s2 || //beq
                    op_funct3 == 1 && reg_s1 != reg_s2 || //bne
                    op_funct3 == 4 && reg_s1_signed <  reg_s2_signed || //blt
                    op_funct3 == 5 && reg_s1_signed >= reg_s2_signed || //bge
                    op_funct3 == 6 && reg_s1 <  reg_s2 || //bltu
                    op_funct3 == 7 && reg_s1 >= reg_s2; //bgeu

//короткие и длинные переходы (jal, jalr)
wire [31:0] rd_jal = pc + 4;
wire [31:0] pc_jal = pc + immediate;
wire [31:0] pc_jalr = reg_s1 + immediate;

//теперь комбинируем результат работы логики разных команд
wire [31:0] rd_result = op_rd == 0 ? 0 : //x0 = 0
						is_op_load ? rd_load :
						is_op_alu || is_op_alu_imm ? rd_alu :
						is_op_load_upper ? rd_load_upper :
						is_op_add_upper ? rd_add_upper :
						is_op_jal || is_op_jalr ? rd_jal :
						regs[op_rd];

wire [31:0] pc_result = (is_op_branch && branch_fired) ? pc_branch :
						is_op_jal ? pc_jal :
						is_op_jalr ? pc_jalr :
						pc + 4;

assign is_wait = is_alu_wait;

integer i;
always@(posedge clock or posedge reset)
begin
	if (reset == 1) begin
		for (i = 0; i < `REG_COUNT; i=i+1) regs[i] = 0;
		pc = 0;
	end
	else if (!is_wait) begin
		regs[op_rd] <= rd_result;
		pc <= pc_result;
	end
end
endmodule


alu.v
`define FAST_MULDIV // деление и умножение за один такт

module RiscVAlu
(
	input clock,
	input reset,
	
	input is_op_alu, //операция с двумя регистрами
	input is_op_alu_imm, //операция с регистром и константой
	input [2:0] op_funct3_in, //код операции
	input [6:0] op_funct7, //код операции
	input [31:0] reg_s1, //первый регистр-операнд
	input [31:0] reg_s2, //второй регистр-операнд
	input [31:0] imm, //константа-операнд
	output [31:0] rd_alu, //результат работы
	output is_alu_wait //надо ли ждать операцию до следующего такта
);

//немного стабилизации входов
wire [2:0] op_funct3_a = (is_op_alu || is_op_alu_imm) ? op_funct3_in : 3'b0;

//обработка (add, sub, xor, or, and, sll, srl, sra, slt, sltu)
//в случае лёгких инструкций вычисляем результат сразу
wire [31:0] alu_operand2 = is_op_alu ? reg_s2 : is_op_alu_imm ? imm : 0;
wire [31:0] rd_alu1 = op_funct3_a == 3'd0 ? (is_op_alu && op_funct7[5] ? reg_s1 - alu_operand2 : reg_s1 + alu_operand2) :
					  op_funct3_a == 3'd4 ? reg_s1 ^ alu_operand2 :
					  op_funct3_a == 3'd6 ? reg_s1 | alu_operand2 :
					  op_funct3_a == 3'd7 ? reg_s1 & alu_operand2 :
					  op_funct3_a == 3'd1 ? reg_s1 << alu_operand2[4:0] :
					  op_funct3_a == 3'd5 ? (op_funct7[5] ? $signed(reg_s1) >>> alu_operand2[4:0] : reg_s1 >> alu_operand2[4:0]) :
					  op_funct3_a == 3'd2 ? $signed(reg_s1) < $signed(alu_operand2) :
					  op_funct3_a == 3'd3 ? reg_s1 < alu_operand2 : //TODO для больших imm проверить
					  0; //невозможный результат

// РАСШИРЕНИЕ M
//обработка (mul, mulh, mulhsu, mulu, div, divu, rem, remu)
//расшифровываем инструкцию
wire is_op_muldiv = is_op_alu && op_funct7[0];
wire [2:0] op_funct3 = is_op_muldiv ? op_funct3_in : 3'b0; //отдельная стабилизация для модуля M
wire is_op_multiply = !op_funct3[2];
wire is_op_mul_signed = !op_funct3[1]; //mul, mulh
//wire is_op_mul_low = op_funct3[1:0] == 0; //mul
wire is_op_mul_extend_sign = op_funct3[1:0] == 2; //mulhsu //умножаем как беззнаковые, но сам знак надо расширить до 64 бит
wire is_op_div_signed = !op_funct3[0]; //div, rem
//wire is_op_remainder = op_funct3[2]; //rem, remu

//для умножения со знаком достаточно убрать знак у короткого сомножителя
//но для деления надо убрать у обоих, поэтому делаем единообразно
wire need_restore_sign = is_op_multiply ? is_op_mul_signed : is_op_div_signed;
wire start_muldiv_sign = need_restore_sign ? reg_s1[31] ^ reg_s2[31] : 1'b0; // = sign(x) * sign(y)
wire rem_sign_start = need_restore_sign ? reg_s1[31] : 1'b0; // = sign(x)
wire [31:0] start_x = (need_restore_sign && $signed(reg_s1) < 0) ? -reg_s1 : reg_s1;
wire [31:0] start_y = (need_restore_sign && $signed(reg_s2) < 0) ? -reg_s2 : reg_s2;

`ifdef FAST_MULDIV

assign is_alu_wait = 0;
wire [31:0] quotient;
wire [31:0] remainder;
wire [31:0] mul_1;
wire [31:0] mul_2;

RiscVFastDiv fd(
		.x(start_x),
		.y(start_y),
		.quotient(quotient),
		.remainder(remainder)
	);

assign {mul_2, mul_1} = start_x * start_y;

// если считать беззнаковым: -a * b = ((1<<32)-a) * b = -a * b + (b << 32)
// для получения mulhsu надо после умножения сделать вычитание беззнакового из старших разрядов
wire [31:0] mulhsu = mul_2 - (reg_s1[31] ? reg_s2 : 0);
wire [63:0] mul_result = start_muldiv_sign ? -{mul_2, mul_1} : {mul_2, mul_1};
wire [31:0] quotient_signed = start_muldiv_sign ? -quotient : quotient;
wire [31:0] remainder_signed = rem_sign_start ? -remainder : remainder;
wire [31:0] rd_mul = op_funct3 == 3'd0 ? mul_result[31:0] : //mul
					op_funct3 == 3'd1 ? mul_result[63:32] : //mulh
					op_funct3 == 3'd2 ? mulhsu            : //mulhsu
					op_funct3 == 3'd3 ? mul_result[63:32] : //mulu
					op_funct3 == 3'd4 ? quotient_signed :   //div
					op_funct3 == 3'd5 ? quotient_signed :   //divu
					op_funct3 == 3'd6 ? remainder_signed :  //rem
					/*op_funct3 == 7 ?*/ remainder_signed;  //remu

`else

//при умножении значения входных переменных могут меняться, поэтому запоминаем их локально
//инструкция запоминается снаружи, её надо сохранять не только для этого блока
//регистры используем для обоих операций
reg [31:0] x, y, r1, r2, r3;
//после начала работы не ориентируемся на входные значения
reg in_progress;
//считаем для беззнаковых, знак меняем в конце
reg muldiv_sign;
//для остатка от деления знак берётся из делимого
reg rem_sign;

//если хоть где-то ноль, результат можно выдать сразу
wire need_wait = is_op_muldiv && reg_s1 && reg_s2;

//инициализируем инструкцию
//перемножение {r3, r2} = {r1 = sign, x} * y
//деление {r3=q, r2=rem} = x / y , r1=msb - счётчик цикла, он же самый значимый бит

wire [31:0] start_msb = start_x[31:24] != 8'b0 ? (1 << 31) : //легковесная подгонка начального счётчика цикла
						start_x[23:16] != 8'b0 ? (1 << 23) :
						start_x[15:8] != 8'b0 ? (1 << 15) :
						(1 << 7);
wire [31:0] start_r1 = !is_op_multiply ? start_msb :
						is_op_mul_extend_sign ? (reg_s1[31] ? -1 : 0) :
						0;

//обрабатываем в цикле
//умножение
wire [31:0] next_mul_y = (y >> 1); //поразрядное умножение
wire [63:0] current_mul_x = {r1, x}; //берём текущий x
wire [63:0] next_mul_x = current_mul_x << 1; //сдвигаем
wire [63:0] next_mul_val = {r3, r2} + (y[0] ? current_mul_x : 0); //и прибавляем к результату
wire mul_end = next_mul_y == 0; //условие окончания умножения

//деление в столбик
//y (делитель) не меняем
//x (делимое) не меняем
wire [31:0] current_msb = r1;
wire [31:0] current_rem = r2;
wire [31:0] current_div_val = r3;
wire [31:0] next_msb = (current_msb >> 1); // выбираем следующий разряд делимого
wire [31:0] next_rem_tmp = {current_rem[30:0], current_msb & x ? 1'b1 : 1'b0}; // приписываем к остатку
//если остаток больше или равен (то есть делится на делитель), то вычитаем
//конкретную цифру подбирать не надо, так как двоичная система
wire [31:0] div_remainder_delta = next_rem_tmp - y;
wire [31:0] next_rem_val = $signed(div_remainder_delta) >= 0 ? div_remainder_delta : next_rem_tmp;
wire [31:0] next_div_val = {current_div_val[30:0], ~div_remainder_delta[31]}; //приписываем разряд к частному
wire div_end = next_msb == 0;

//комбинируем значения для заполнения регистров
//в теории регистры можно переставить, сдвиг и условие выхода кажутся похожими
wire [31:0] next_x = is_op_multiply ? next_mul_x[31:0] : x;
wire [31:0] next_y = is_op_multiply ? next_mul_y : y;
wire [31:0] next_r1 = is_op_multiply ? next_mul_x[63:32] : next_msb;
wire [31:0] next_r2 = is_op_multiply ? next_mul_val[31:0] : next_rem_val;
wire [31:0] next_r3 = is_op_multiply ? next_mul_val[63:32] : next_div_val;
wire divmul_end = is_op_multiply ? mul_end : div_end; //блок умножения закончил вычислять
wire next_in_progress = in_progress && !divmul_end; //когда законил, выключаем регистр активности блока

wire [63:0] mul_result = muldiv_sign ? -next_mul_val : next_mul_val;
wire [31:0] div_result = muldiv_sign ? -next_div_val : next_div_val;
wire [31:0] rem_result = rem_sign ? -next_rem_val : next_rem_val;
wire [31:0] rd_mul = !in_progress ? 0 : //на нулевом такте всё равно может быть только ноль
					!divmul_end ? 0 : //пока процесс идёт, не качаем затворы
					op_funct3 == 3'd0 ? mul_result[31:0] : //mul
					op_funct3 == 3'd1 ? mul_result[63:32] : //mulh
					op_funct3 == 3'd2 ? mul_result[63:32] : //mulsu
					op_funct3 == 3'd3 ? mul_result[63:32] : //mulu
					op_funct3 == 3'd4 ? div_result : //div
					op_funct3 == 3'd5 ? div_result : //divu
					op_funct3 == 3'd6 ? rem_result : //rem
					/*op_funct3 == 7 ?*/ rem_result;  //remu

//когда нельзя переходить к следующей инструкции
assign is_alu_wait = !in_progress ? need_wait : !divmul_end;

wire divmul_active = need_wait || in_progress;
always@(posedge clock or posedge reset)
begin
	if (reset == 1) begin
		x <= 0;
		y <= 0;
		r1 <= 0;
		r2 <= 0;
		r3 <= 0;
		in_progress <= 0;
	end
	else if (divmul_active) begin
		if (!in_progress) begin // на нулевом такте просто запоминаем значения
			in_progress <= 1'b1;
			x <= start_x;
			y <= start_y;
			r1 <= start_r1; //расширение первого сомножителя знаком до 64 бит или счётчик цикла
			r2 <= 0; //младшие биты произведения или остаток
			r3 <= 0; //старшие биты произведения или частное
			muldiv_sign <= start_muldiv_sign; //надо ли в конце сменить знак
			rem_sign <= rem_sign_start;
		end
		else begin
			in_progress = next_in_progress;
			x <= next_x;
			y <= next_y;
			r1 <= next_r1;
			r2 <= next_r2;
			r3 <= next_r3;
		end
	end
end

`endif

//выдаём наружу результат
assign rd_alu = is_op_muldiv ? rd_mul :
					rd_alu1;

endmodule


Код написан, пошли теперь смотреть, к чему привели наши старания, для этого добавляем новый файл в строку симуляции.
iverilog.exe -I ../core -o tmp_sim core_tb.v ../core/core.v ../core/fast_div.v

И так, до появления модуля умножения тест CoreMark работал 77 периодов, то есть выдавал 1.3 CM/МГц. Запускаем с многотактовым блоком умножения и деления.





37 периодов, 2.7 CM/Mhz, да это же на уровне современных микроконтроллеров. Деление 3 байта на 1 байт заняло 17 тактов. Вроде бы медленно, но по сравнению с программным делением ощутимо быстрее. Теперь переключаем на однотактный блок.





31 период, 3.2 CM/Mhz. Прирост уже не такой большой, так что если делать ASIC, однотактное умножение не так уж и нужно, а для вычислительных задач можно сделать нечто более приспособленное к поточным вычислениям. Ещё важный пункт, деления почти не вызываются во время теста, так что влияние нового алгоритма лучше проверять на чём-то другом.

И ещё один вопрос — это реализация на ПЛИС. Многотактовый алгоритм практически не снизил скорость работы процессора — как было 50 МГц на EP4CE6, так и осталось — и занял примерно 700 LE. А однотактное деление уменьшает тактовую частоту до 5 МГц, занимая 7000 LE, и даже если уменьшить число пройденных бит до 1, выходит 20 МГц, где-то на вычисленном значении start_y запас времени исякает. В общем, без аппаратной реализации тут никак.

Комментарии (0)