1. Введение

  2. Описание модуля с фиксированными параметрами — CordicSimple_v1

  3. Небольшая модернизация — CordicSimple_v2

  4. Описание параметризируемого модуля — CordicParam_v1

  5. Модернизация: возможность увеличения производительности — CordicParam_v2

  6. Итоги

1. Введение

В далеком 2011 году автором была опубликована статья «Реализация CORDIC‑алгоритма на ПЛИС» [1]. В той статье приводится сначала математическое описание алгоритма, его суть. Показан пример расчета поворота вектора на плоскости сначала «на бумажке» согласно алгоритму, а затем сравнение результатов с расчетом «по калькулятору». Затем, показано создание структурной схемы проекта с rtl‑описанием CORDIC‑алгоритма и приведены листинги каждого модуля. Помимо этого были приведены основы создания проекта в среде ModelSim.

Автор считает, что эта статья оказалась полезной для новичков в области программирования ПЛИС, так как на протяжении долгого периода времени, после публикации статьи приходили письма с вопросами и уточнениями на данную тему. Даже сейчас я часто встречаю на различных форумах на тему ПЛИС ссылки на данную работу. Но! Если математическая часть алгоритма однозначно является полезной, а также основы создания и структурированного ведения проекта для новичка, то использовать данное rtl‑описание в различных проектах, адаптируя модули проекта по свои нужды крайне неудобно и неуклюже. По крайней мере, автор, когда заглядывает в это свое создание в прошлом, морщится и чувствует себя неловко.

Поэтому появилось желание представить тот модуль CORDICа (для генерации гармонического сигнала в сфере радиолокации и связи), который автор использует в данный момент в различных проектах. Весь алгоритм реализован в одном sv‑модуле, а, значит, его намного легче переносить из проекта в проект. Также он более прост в восприятии.

После этого автор попробует его максимально параметризировать.

Считаю нужным повторить очень кратко математическую часть алгоритма CORDIC. Данный алгоритм позволяет простейшими операциями реализовать поворот вектора на плоскости (Рис.1):

Рисунок 1
Рисунок 1

Короче говоря, зная координаты (X0, Y0) исходного положения вектора, если стоит задача повернуть вектор на угол Z, то координаты нового положения вектора (X1, Y1), можно вычислить через следующие тригонометрические формулы (Рис.2):

Рисунок 2
Рисунок 2

Основное внимание в CORDIC-алгоритме следует уделить именно tan(Z). В диапазоне угла Z от 0° до 45° значение tan(Z) принимает значения от 0 до 1 соответственно. Если разбить один разовый поворот, на несколько со значениями tan(Z) :

tan(Z)

1

1/2

1/4

1/8

1/16

1/32

20

2-1

2-2

2-3

2-4

2-5


Z

45°

~26°

~14°

~7°

~4°

~2°

то можно реализовать последовательное приближение к целевому положению вектора, используя последовательные вычисления типа (Рис.3):

Рисунок 3
Рисунок 3

а первые 3 поворота вектора на некоторый угол могут выглядеть следующим образом (Рис.4):

Рисунок 4
Рисунок 4

Листинг одного из вариантов модуля, реализующего CORDIC-алгоритм, которым пользуется автор – CordicSimple_v1 можно посмотреть на … . А в рамках данной статьи разберем отдельные части rtl-описания CordicSimple_v1.

2. Описание модуля с фиксированными параметрами — CordicSimple_v1

Шапка модуля CordicSimple_v1 (Листинг 1):

module CordicSimple_v1 (
	input clk,
	input on,
	input [31:0] step,
	input [15:0] level,
	output logic [15:0] X,
	output logic [15:0] Y
);

step — величина шага фазы за один такт clk. В мгновенном понимании — это угол целевого вектора, на который нужно повернуть исходный. Также можно сказать, чем больше значение step, тем больше будет частота сгенерированного гармонического сигнала.

level — требуемая амплитуда сгенерированного гармонического сигнала. В работе [1] было упомянуто про коэффициент деформации (≈0.607), на который нужно умножить рассчитанные результаты X и Y, либо, сразу взять максимальное значение (если мы хотим получить максимально возможную амплитуду) разрядности level, умножить на коэффициент деформации и задать как значение level. Так и поступим.

X, Y — синусоидальная и косинусоидальная компоненты гармонического сигнала, их еще называют I и Q компоненты квадратурного сигнала. А в мгновенном значении — координаты целевого вектора после процедуры поворота по алгоритму CORDIC.

Объявления внутренних сигналов, используемых в модуле (Листинг 2):

logic [31:0] acc;
logic [15:0] ang [0:16];

wire [1:0] quad = acc[31:30];

logic signed [15:0] X0, Y0;

logic signed [15:0] Xs [0:16];
logic signed [15:0] Ys [0:16];
logic signed [17:0] Zs [0:16];
wire signed [17:0] As [0:16];

acc — аккумулятор фазы. С каждым тактом clk инкрементируется на величину step.

ang — 17-ти элементный 16-ти разрядный сдвиговый регистр, содержащий целевое значение угла по ходу конвейера.

quad — номер четверти на координатной плоскости, назначаются 2-мя старшими битами аккумулятора фазы acc. Суть следующая (Рис.5): в начале работы вектор полностью ложится на ось X. Далее, в результате инкремента фазы в acc, вектор начнет перемещаться в координатной четверти I (quad = 2'b01). Когда старшие 2 бита acc установятся в 2«b01, начальный вектор полностью ляжет на ось Y и начнется перемещение вектора по четверти II (quad = 2'b01). В четверти III изначально вектор ляжет снова на ось X, но с отрицательным значением (quad = 2'b10). Затем в четверть IV на ось Y, также с отрицательным значением (quad = 2'b11). Звучит непонятно, иллюстрация ниже поможет восприятию, так как гармонический сигнал можно назвать движением точки по кругу, расстянутом во времени.»

Рисунок 5
Рисунок 5

X0, Y0 — исходное положение вектора в начале конвейера алгоритма

Xs, Ys — 17 регистров конвейера, в котором идет расчет координат целевого вектора на каждом шаге итерации.

As — Сдвиговый регистр с 17-ю ячейками, разрядностью 18 бит. В этом сдвиговом регистре «толкается» по конвейеру угол (фаза) целевого вектора. Разрядность больше 2 бита: 1 разряд за счет того, что в процессе расчёта вектор может «забегать» за 90° и 0°, 2й разряд — для знака.

Zs — 17 регистров конвейера, в котором идет поворот вектора (сохраняется текущее в расчете значение фазы). Разрядность такая же как у As.

Идем дальше, ниже представлено назначение регистров текущего угла ang с расширением на 2 разряда регистрам As, а также Инициализация начальных значений регистров. Это сделано только для моделирования. Синтезатор блоки initial игнорирует (Листинг 3).

assign As[0]  = {2'h0,ang[0]};
assign As[1]  = {2'h0,ang[1]};
assign As[2]  = {2'h0,ang[2]};
assign As[3]  = {2'h0,ang[3]};
assign As[4]  = {2'h0,ang[4]};
assign As[5]  = {2'h0,ang[5]};
assign As[6]  = {2'h0,ang[6]};
assign As[7]  = {2'h0,ang[7]};
assign As[8]  = {2'h0,ang[8]};
assign As[9]  = {2'h0,ang[9]};
assign As[10] = {2'h0,ang[10]};
assign As[11] = {2'h0,ang[11]};
assign As[12] = {2'h0,ang[12]};
assign As[13] = {2'h0,ang[13]};
assign As[14] = {2'h0,ang[14]};
assign As[15] = {2'h0,ang[15]};
assign As[16] = {2'h0,ang[16]};

initial
begin
	acc = '0;
	for(int i=0; i<17; i++) begin
		Zs[i] = '0;
		ang[i] = '0;
	end
end

Ниже реализован процесс инкремента аккумулятора фазы acc и схема сдвигового регистра целевого угла ang соответственно (Листинг 4):

always @ (posedge clk)
	begin : acc_incr
		if (on) acc <= acc + step;
		else acc <= '0;
	end

	always @ (posedge clk)
	begin : ang_pipe
		ang[0] <= acc[29:14];	
		for(int i=0; i<16; i++) 
			ang[i+1] <= ang[i];
	end

Может возникнуть вопрос: почему аккумулятор acc имеет разрядность 32 бита, а в конвейер для расчета «толкается» только старшие 16 бит. Это сделано, для более точного расчета следующего положения вектора при генерации гармонического сигнала. Чем больше разрядность аккумулятора фазы acc, тем с меньшим шагом будет возможно генерировать гармонический сигнал.

Ниже представлен процесс выбора, в какой координатной четверти пойдет расчет целевого вектора, в зависимости от значения quad (вспоминаем рисунок 5) (Листинг 5):

always @ (posedge clk)
	begin : quarter
		if(quad == 2'b00) begin
			X0 <= level;
			Y0 <= '0; end
		else if(quad == 2'b01) begin
			X0 <= '0;
			Y0 <= level; end
		else if(quad == 2'b10) begin
			X0 <= ~level;
			Y0 <= '0; end
		else begin
			X0 <= '0;
			Y0 <= ~level; end
	end

Дальше начинается описание конвейера расчета положения вектора. Первый элемент конвейера выглядит следующим образом (Листинг 6):

always @ (posedge clk)
	begin : iter_0
		Zs[0] <= 18'h08000;
		Xs[0] <= X0 - Y0;
		Ys[0] <= Y0 + X0;
	end

Теперь, вспомним формулу на рисунке 3, которую я привел в своей работе [1], математические выкладки которой, автор в свою очередь почерпнул из [2]. Направление поворота выражено через σi, которая в свою очередь определяется zi. zi — является текущим углом вектора (фазой) в ходе расчета, но важна разница между целевым углом и текущим углом на данном этапе расчета, это влияет на направление следующего поворота.

Можно объяснить более просто, взглянув на рисунок 4. Там показаны первые три итерации алгоритма CORDIC. Нам, в конечном итоге, необходимо повернуть вектор, например, на угол 55°. В первой итерации вектор поворачивается на угол 45° — он меньше чем целевой угол, поэтому σi следующей итерации будет отрицательным, чтобы направление следующего поворота вектора не поменялось. Далее в 3й итерации, получилось, что угол повернут на более чем целевой, поэтому направление поворота нужно изменить.

Итак, в листинге 6 приведен код 0-й итерации (мы же нормальные люди — считаем с нуля…). Так как вначале расчета исходный вектор лежит полностью на одной из осей, то первая итерация всегда будет иметь положительной направление — значение фазы вектора первой итерации Zs[0] = 18’h08000; Откуда оно взялось? Координатная четверть помещается в 16-ти разрядное число. Поворот первой итерации составляет 45°, т.е. половину от от четверти, а значит и от максимального значения 16-ти разрядного числа: 216 / 2 = 215 = 32768 dec = 16’h8000 hex. Именно это значение будет являться arctan(2-0) → 45°.

Рассмотрим код следующей итерации алгоритма (Листинг 7):

always @ (posedge clk)
	begin : iter_1
		if(As[1] < Zs[0]) begin
			Zs[1] <= Zs[0] - 18'h04b90;
			Xs[1] <= Xs[0] + (Ys[0] >>> 1);
			Ys[1] <= Ys[0] - (Xs[0] >>> 1); end
		else begin
			Zs[1] <= Zs[0] + 18'h04b90;
			Xs[1] <= Xs[0] - (Ys[0] >>> 1);
			Ys[1] <= Ys[0] + (Xs[0] >>> 1); end
	end

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

Величина поворота этой итерации рассчитывается следующим образом: 215 * arctan(2-1) / arctan(20) ≈ 19344 dec = 16’h4b90 hex.

Все итерации приводить в тексте не автор не будет, приведем следующую и две последние итерации (Листинг 8):

always @ (posedge clk)
	begin : iter_2
		if(As[2] < Zs[1]) begin
			Zs[2] <= Zs[1] - 18'h027ed;
			Xs[2] <= Xs[1] + (Ys[1] >>> 2);
			Ys[2] <= Ys[1] - (Xs[1] >>> 2); end
		else begin
			Zs[2] <= Zs[1] + 18'h027ed;
			Xs[2] <= Xs[1] - (Ys[1] >>> 2);
			Ys[2] <= Ys[1] + (Xs[1] >>> 2); end
	end

................................................

	always @ (posedge clk)
	begin : iter_15
		if(As[15] < Zs[14]) begin
			Zs[15] <= Zs[14] - 18'h00001;
			Xs[15] <= Xs[14] + (Ys[14] >>> 15);
			Ys[15] <= Ys[14] - (Xs[14] >>> 15); end
		else begin
			Zs[15] <= Zs[14] + 18'h00001;
			Xs[15] <= Xs[14] - (Ys[14] >>> 15);
			Ys[15] <= Ys[14] + (Xs[14] >>> 15); end
	end

	always @ (posedge clk)
	begin : iter_16
		if(As[16] < Zs[15]) begin
			Xs[16] <= Xs[15] + (Ys[15] >>> 16);
			Ys[16] <= Ys[15] - (Xs[15] >>> 16); end
		else begin
			Xs[16] <= Xs[15] - (Ys[15] >>> 16);
			Ys[16] <= Ys[15] + (Xs[15] >>> 16); end
	end

В процессе iter_2 величина поворота рассчитывается следующим образом: 215 * arctan(2-2) / arctan(20) ≈ 10 221 dec = 16«h27ed hex. »

В процессе iter_15 величина поворота рассчитывается: 215 * arctan(2-15) / arctan(20) ≈ 1 dec = 16«h0001 hex.»

В последнем iter_16 расчет следующего поворота уже не требуется, так как итерация — последняя.

Теперь следует показать, как выглядит результат работы модуля CordicSimple_v1 в симуляторе (рис. 6). Исходный код и тестбенч для моделирования приведены по ссылке.

Рисунок 6
Рисунок 6

Здесь, в качестве примера, был сгенерирован квадратурный сигнал частотой 2 МГц. Тактовая частота clk = 100 МГц. Шаг аккумулятора фазы acc = 2 / 100 * 2^32 = 0x051EB852.

Не являясь мастером по MatLAB, все-таки интересно было написать простенький скрипт, вычисляющий спектр сгенерированного сигнал с помощью БПФ с количеством точек 4096 (риc. 7):

Рисунок 7
Рисунок 7

Так как количество точек БПФ = 4096, частота дискретизации составляет 100 МГц, таким образом шаг по частоте рассчитанного спектра равна 100 МГц / 4096 ≈ 24,4 кГц. Погрешностью по частоте является половина шага ≈ 12,2 кГц. В расчете MatLAB отличие составило 1,95 кГц — в пределах погрешности.

3. Небольшая модернизация — CordicSimple_v2

В целом модуль алгоритм CORDIC готов, но единственно, что меня немного смущает, это в данном случае входной уровень level рассчитывается исходя из половины диапазона 16-ти бит, так как речь идет об амплитуде, а не пик-ту-пик. Чтобы считать level исходя из 16-ти битного значения, нужно увеличить разрядность регистров Xs, Ys, а также изменить строки 286 и 290 следующим образом (Листинг 9):

always @ (posedge clk)
	begin : data
		X <= Xs[16][16:1] + Xs[16][0];
		Y <= Ys[16][16:1] + Ys[16][0];
	end

Таким образом, мы увеличили точность установки амплитуда в два раза. Хотя это может быть и необязательно, так как 15-ти бит вполне хватит.

Полный текст обновленного модуля (назовем его CordicSimple_v2) и его симуляцию можно посмотреть тут.

На данный момент автор привел rtl-описание модуля CORDIC-алгоритма, который использует сам. А теперь разработаем параметризируемый (!) модуль CORDIC-алгоритма, чтобы можно было применять один тот rtl-файл для различных ситуаций имеющие разные параметры. Назовем его CordicParam_v1, да, версия №1, так как в дальнейшем, возможно, будет обновляться, видоизменяться, улучшаться...

4. Описание параметризируемого модуля — CordicParam_v1

Шапка модуля CordicParam_v1 (Листинг 10):

module CordicParam_v1
#(
	parameter int DAT_W = 16,
	parameter int ACC_W = 32,
	parameter int NUM_I = 16
)
(
	input clk,
	input on,
	input [ACC_W-1:0] step,
	input [DAT_W-1:0] level,
	output logic [DAT_W-1:0] X,
	output logic [DAT_W-1:0] Y
);

Начнем с параметров модуля. Первым параметром является DAT_W — разрядность выходных, сгенерированных значений X и Y. Следующий параметр ACC_W — размерность аккумулятора фазы. Как было сказано выше, чем больше разрядность аккумулятора фазы, тем с более мелким шагом можно менять частоту сгенерированного сигнала. Далее, NUM_I — количество итераций (поворотов вектора) конвейера. Опять же, как было уже сказано, чем больше итераций, тем ближе в процессе расчета можно подобраться к положению целевого вектора.

Список портов модуля CordicParam_v1 такой же как и у непараметризируемого модуля CordicSimple_v2. Разрядность шага по фазе step должно определяться разрядностью аккумулятора фазы ACC_W. Будет логично разрядность уровня выходного сигнала level сделать таким же как разрядность выходного гармонического сигнала, именно этим и отличаются непараметризируемые модули CordicSimple_v1 и CordicSimple_v2.

Далее идет список параметризированных сигналов (Листинг 11), соответствующий листингу 2 не параметризированного модуля:

    logic [ACC_W-1:0] acc;
	logic [DAT_W-1:0] ang [0:NUM_I];

	wire [1:0] quad = acc[ACC_W-1:ACC_W-2];

	logic signed [DAT_W:0] X0, Y0;

	logic signed [DAT_W:0] Xs [0:NUM_I];
	logic signed [DAT_W:0] Ys [0:NUM_I];
	logic signed [DAT_W+1:0] Zs [0:NUM_I];
	wire signed [DAT_W+1:0] As [0:NUM_I];

Разрядность текущего угла ang сооветствует размерности шины, а количество их соответствует длине итераций, что логично. Номер четверти quad определяется двумя старшими разрядами аккумулятора acc. Разрядности X0, Y0, Xs, Ys и длина векторов Xs, Ys, также интуитивно понятны. Разрядность целевого угла As на 2 бита больше, чем разрядность шины.

Ниже представлен параметризированный код (Листинг 12), соответствующий листингу 3 :

    generate
		genvar i;
		for (i = 0; i < NUM_I; i = i + 1) begin : assin_ang
			assign As[i] = {2'b00,ang[i]};
		end
	endgenerate

	initial
	begin
		acc = '0;
		for(int i=0; i<NUM_I; i++) begin
			Zs[i] = '0;
			ang[i] = '0;
		end
	end

Т.е. в строках 1...6 представленно в более компактном виде расширение ang до As с присвоением дополнительных 2х разрядов. А в строках 8...15 представлен не синтезируемый блок initial для инициализации начальных значений необходимых симулятору.

Ниже будет представлена часть rtl-кода, назначение которого является расчет поворотных коэффициентов для каждого элемента конвейера (на каждом этапе поворота вектора). Вспомним модуль CordicSimple_v1 или v2 (для 16-ти разрядной шины данных), где величина поворота рассчитывается по формуле: 215 * arctan(2-i) / arctan(20), где i – номер итерации. Таким образом, записав константы в виде параметров, можно генерировать поворотные коэффиценты для любой шины данных (Листинг 13):

localparam logic [31:0] parMax = 2**(DAT_W-1);
	localparam logic [31:0] atan45 = 0.78539816 * parMax;
	localparam logic [31:0] atan[26] = '{
		0.46364761 * parMax,
		0.24497866 * parMax,
		0.12435499 * parMax,
		0.06241881 * parMax,
		0.03123983 * parMax,
		0.01562373 * parMax,
		… 
		0.00000024 * parMax,
		0.00000012 * parMax,
		0.00000006 * parMax,
		0.00000003 * parMax,
		0.00000001 * parMax
	};

	logic [DAT_W+1:0] Rot [0:NUM_I];

	generate
		genvar k;
		for (i = 0; i < NUM_I-1; i = i + 1) begin : assin_rot
			assign Rot[i] = parMax * atan[i] / atan45;
		end
	endgenerate

мы получим поворотные коэффициенты для всех итераций конвейера. Например, в модулях CordicSimple_v1 и v2 в листинге 7 указан в виде константы поворотный коэффициент со значением 18'h04b90. Попробуем рассчитать его же с помощью кода, описанного в листинге выше, если принять параметр DAT_W = 16:

Rot[i] = 215 atan[0] / atan45 = 32768 0.46364761 / 0.78539816 ≈ 19344d = 4B90h. Все сходится.

32-х разрядов, возможно, в каких-то случаях окажется мало. Это дискуссионный момент, которые будет обсужден в конце статьи.

Ниже представлен параметризированный вариант листинга 4:

	always @ (posedge clk)
	begin : acc_incr
		if (on) acc <= acc + step;
		else acc <= '0;
	end

	always @ (posedge clk)
	begin : ang_pipe
		ang[0] <= acc[ACC_W-3:ACC_W-DAT_W-2];	
		for(int i=0; i<NUM_I; i++) 
			ang[i+1] <= ang[i];
	end

Строки 1...5 не поменялись, здесь производится инкремент аккумулятора фазы acc с заданным шагом step. Ну, а в строках 7...12 описан параметризированный код продвижения целевого угла по итерациям конвейера. Целевым углом является регистры шириной DATA_WIDTH, отступив 2 старших разряда. Как мы помним, 2 старших разряда определяют координатную четверть (quad).

Вот как раз ниже и представлен выбор координатной четверти (листинг 15) в зависимости от 2-х старших разрядом acc. Код не отличается от листинга 5:

	always @ (posedge clk)
	begin : quarter
		if(quad == 2'b00) begin
			X0 <= level;
			Y0 <= '0; end
		else if(quad == 2'b01) begin
			X0 <= '0;
			Y0 <= level; end
		else if(quad == 2'b10) begin
			X0 <= ~level;
			Y0 <= '0; end
		else begin
			X0 <= '0;
			Y0 <= ~level; end
	end

Далее, начнем описывать итерации конвейера. Отдельно описывается самая 1-я (нулевая) итерация (листинг 16):

	always @ (posedge clk)
	begin : iter_0
		Zs[0] <= 1 << (DAT_W-1);
		Xs[0] <= X0 - Y0;
		Ys[0] <= Y0 + X0;
	end

Данный код почти идентичен листингу 6, за исключением присвоение начального значения угла Zs[0], которому присваивается половина от максимального значения DATA_WIDTH. Как было сказано выше, при разработке модуля CordicSimple_v1, первый поворот всегда делается на 45 градусов, а это половина от максимального значения разрядности шины. Например, при 16-ти разрядной шине (максимальное значение 216 - 1 ) под 45º будет пониматься значение 16'h8000, т. е. 215 , т.е. DATA_WIDTH-1 в параметризируемом модуле.

Далее, используя конструкцию generate приведено описание всех остальных итераций конвейера кроме самой последней (листинг 17):

	generate
		genvar j;
		for (j = 0; j < NUM_I-1; j = j + 1) begin : iter
			always @ (posedge clk)
			begin : iter
				if(As[j+1] < Zs[j]) begin
					Zs[j+1] <= Zs[j] - Rot[j];
					Xs[j+1] <= Xs[j] + (Ys[j] >>> j+1);
					Ys[j+1] <= Ys[j] - (Xs[j] >>> j+1); end
				else begin
					Zs[j+1] <= Zs[j] + Rot[j];
					Xs[j+1] <= Xs[j] - (Ys[j] >>> j+1);
					Ys[j+1] <= Ys[j] + (Xs[j] >>> j+1); end
			end
		end
	endgenerate

Здесь, с помощью конструкции generate … endgenerate мы описали то, что было описано в строках 91...278 в модуле CordicSimple_v2. Очень компактно. Также следует обратить внимание, что в регистрах Rot[j] располагаются поворотные коэффициенты, рассчитанные в листинге 13.

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

	always @ (posedge clk)
	begin : iter_last
		if(As[NUM_I] < Zs[NUM_I-1]) begin
			Xs[NUM_I] <= Xs[NUM_I-1] + (Ys[NUM_I-1] >>> NUM_I);
			Ys[NUM_I] <= Ys[NUM_I-1] - (Xs[NUM_I-1] >>> NUM_I); end
		else begin
			Xs[NUM_I] <= Xs[NUM_I-1] - (Ys[NUM_I-1] >>> NUM_I);
			Ys[NUM_I] <= Ys[NUM_I-1] + (Xs[NUM_I-1] >>> NUM_I); end
	end

Да, можно было бы и последнюю итерацию «засунуть» в конструкцию generate (предыдущий листинг), использовав if...else, но автор решил не усложнять и выделить последнюю итерацию, немного отличающуюся от остальных в отдельные строки кода.

Так как параметризируемый модуль CordicParam_v1 разрабатывается из непараметризируемого CordicSimple_v2, где присутствует код в листинге 9 , то приведем его параметризируемую версию (листинг 19):

	always @ (posedge clk)
	begin : data
		X <= Xs[NUM_I][DAT_W:1] + Xs[NUM_I][0];
		Y <= Ys[NUM_I][DAT_W:1] + Ys[NUM_I][0];
	end

Полную текст модуля доступен тут.

Итак, следует теперь сравнить результат симуляции модулей CordicSimple_v2 и CordicParam_v1 с одинаковыми параметрами (рис.8):

 Рисунок 8 - Симуляция, сравнение CordicParam_v1 и CordicSimple_v2. DATA_WIDTH = 16, ACC_WIDTH = 32, NUM_ITERATIONS = 16.
Рисунок 8 - Симуляция, сравнение CordicParam_v1 и CordicSimple_v2. DATA_WIDTH = 16, ACC_WIDTH = 32, NUM_ITERATIONS = 16.

Как видим, результат практически идентичен, максимальное отклонение, которое автор визуально заметил, составляет 2 бита. Вероятно это связано именно с приведением поворотных коэффициентов как 32-х разрядному числу. Повторюсь, в конце статьи, в эпилоге этот момент будет упомянут.

Теперь, хотелось бы сравнить спектр сгенерированного гармонического сигнала, показанного на рисунке … со спектром гармонического сигнала, полученного с помощью параметризируемого модуля CordicParam_v1 (рис.9):

Рисунок 9
Рисунок 9

Можно сравнить уровень паразитных составляющих сгенерированного гармонического сигнала модулей CordicSimple_v1 (рис. 7) и CordicParam_v1 (рис. 9).

Итак, что получено с технической точки зрения: количество занимаемых логических элементов и быстродействие? Данная информация приведена в таблице 1:

Таблица 1

Вариант модуля

CordicParam_v1

Количество логических ячеек

1867

Количество регистров

1197

Максимальная частота работы

177.81 МГц

При компиляции и анализа проекта, была выбрана ПЛИС фирмы Intel (Altera), семейства Cyclone IV – EP4CE6E22I7.

5. Модернизация — возможность увеличения производительности — CordicParam_v2

Чем можно усовершенствовать параметризируемый модуль — будущий CordicParam_v2. В первую очередь можно попытаться увеличить максимально возможную частоту работы модуля. Это можно сделать, уменьшив «длину» комбинационных цепочек, расположенных между регистрами конвейера.

Вот так выглядит одна цепочка конвейера (рис 10):

Рисунок 10 - Цепочка элемента конвейера модуля CordicParam_v1
Рисунок 10 - Цепочка элемента конвейера модуля CordicParam_v1

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

В шапке модуля CordicParam_v2 добавится параметр PERF. При значении данного параметра равным 0, модуль будет повторять логику CordicParam_v1, а при значении параметра равным 1, каждая ячейка конвейера будет разделяться на две, уменьшая длину комбинационной логики в каждой каждой итерации (листинг 20):

	module CordicParam_v2
	#(
		parameter int DAT_W = 16,
		parameter int ACC_W = 32,
		parameter int NUM_I = 16,
		parameter bit PERF = 0 	//1 = yes, 0 = no
	)
	(
	input clk,
	input on,
	input [ACC_W-1:0] step,
	input [DAT_W-1:0] level,
	output logic [DAT_W-1:0] X,
	output logic [DAT_W-1:0] Y
	);

Так как длина конвейера увеличивается, то увеличивается количество регистров, например помимо текущего угла ang, необходимо объявить ang_buf, такой же разрядности.

Аналогично X и Y квадратурами. Например, помимо объявления Xs, необходимо объявить регистры по двум возможным направлениям поворота – Xs_p, Xs_n с такой же разрядностью и глубиной. То же касается составляющей Ys и текущим углом Zs.

Главные изменения в коде подвергнется листинг 17 модуля первой версии CordicParam_v1 (листинг 21):

	generate
		genvar j;
		for (j = 0; j < NUM_I-1; j = j + 1) begin : iter
			if (!PERF)
				always @ (posedge clk)
				begin : iter
					if(As[j+1] < Zs[j]) begin
						Zs[j+1] <= Zs[j] - Rot[j];
						Xs[j+1] <= Xs[j] + (Ys[j] >>> j+1);
						Ys[j+1] <= Ys[j] - (Xs[j] >>> j+1); end
					else begin
						Zs[j+1] <= Zs[j] + Rot[j];
						Xs[j+1] <= Xs[j] - (Ys[j] >>> j+1);
						Ys[j+1] <= Ys[j] + (Xs[j] >>> j+1); end
				end
			else 
				always @ (posedge clk)
				begin : iter
					Zs_p[j+1] <= Zs[j] - Rot[j];		
					Zs_n[j+1] <= Zs[j] + Rot[j];		
					Xs_p[j+1] <= Xs[j] + (Ys[j] >>> j+1);
					Ys_p[j+1] <= Ys[j] - (Xs[j] >>> j+1);
					Xs_n[j+1] <= Xs[j] - (Ys[j] >>> j+1);
					Ys_n[j+1] <= Ys[j] + (Xs[j] >>> j+1);
					Zs_buf[j+1] <= Zs[j];

					if(As[j+1] < Zs_buf[j+1]) begin
						Zs[j+1] <= Zs_p[j+1];
						Xs[j+1] <= Xs_p[j+1];
						Ys[j+1] <= Ys_p[j+1]; end 
					else begin
						Zs[j+1] <= Zs_n[j+1];
						Xs[j+1] <= Xs_n[j+1];
						Ys[j+1] <= Ys_n[j+1]; end
				end
		end
	endgenerate

По лиcтингу 21 видно, что при значении параметра PERF равным 0, код конвейера повторяет код из модуля CordicParam_v1. А при значении PERF равным 1, каждый элемент конвейера разбивается на два такта. На первом такте производится расчет обоих возможных направлений поворота: значения Zs, Xs, Ys как в случае если текущий угол меньше целевого, так и больше. А на втором такте уже просто идет защелкивание одного из вариантов в зависимости от направления текущего поворота в конвейере. Очевидно, что количество затраченных ресурсов ПЛИС увеличивается, но посмотрим в дальнейшем будет ли это иметь какой-либо смысл...

С помощью RTL-Viewer можно посмотреть как будет выглядеть элемент конвейера с установленным параметром PERF равным 1, т. е. с удвоенной длинной конвейера (рис. 11):

Рисунок 11
Рисунок 11

Как видно, между регистрами конвейера, например Xs[6] и Xs[7], появились регистры Xs_n[7] и Xs_p[7], которые описывают оба возможные направления расчета на следующем этапе. Аналогичная ситуация с регистрами Ys и Zs.

Осталось описать код для последней итерации, т. е. доработать листинг 18 с учетом параметра PERF (листинг 22):

	generate
		if (!PERF)
			always @ (posedge clk)
			begin : iter_last
				if(As[NUM_I] < Zs[NUM_I-1]) begin
					Xs[NUM_I] <= Xs[NUM_I-1] + (Ys[NUM_I-1] >>> NUM_I);
					Ys[NUM_I] <= Ys[NUM_I-1] - (Xs[NUM_I-1] >>> NUM_I); end
				else begin
					Xs[NUM_I] <= Xs[NUM_I-1] - (Ys[NUM_I-1] >>> NUM_I);
					Ys[NUM_I] <= Ys[NUM_I-1] + (Xs[NUM_I-1] >>> NUM_I); end
			end
		else
			always @ (posedge clk)
			begin : iter
				Xs_p[NUM_I] <= Xs[NUM_I-1] + (Ys[NUM_I-1] >>> NUM_I);
				Ys_p[NUM_I] <= Ys[NUM_I-1] - (Xs[NUM_I-1] >>> NUM_I);
				Xs_n[NUM_I] <= Xs[NUM_I-1] - (Ys[NUM_I-1] >>> NUM_I);
				Ys_n[NUM_I] <= Ys[NUM_I-1] + (Xs[NUM_I-1] >>> NUM_I);
				Zs_buf[NUM_I] <= Zs[NUM_I-1];

				if(As[NUM_I] < Zs_buf[NUM_I]) begin
					Xs[NUM_I] <= Xs_p[NUM_I];
					Ys[NUM_I] <= Ys_p[NUM_I]; end 
				else begin
					Xs[NUM_I] <= Xs_n[NUM_I];
					Ys[NUM_I] <= Ys_n[NUM_I]; end
			end
	endgenerate

Код написан, статья подходит к своему логическому завершению, осталось только показать результат симуляции работы модуля CordicParam_v2 с разными значениями параметра PERF (рис 12). В этой симуляции экземпляр модуля CordicParam_v2 с параметром PERF = 1 был запущен на 17 тактов клока раньше, чем экземпляр модуля CordicParam_v2 с параметром PERF = 0, по числу дополнительных промежуточных элементов конвейера (разница между курсорами 1 и 2). Не смотря на то, что длина конвейера увеличилась, результаты сгенерированного гармонического сигнала не отличается. Об этом указывает данные сигналов при положении курсора 3.

Рисунок 12
Рисунок 12

Код модуля CordicParam_v2 доступен по ссылке.

6. Итоги

Осталось сравнить затраченные ресурсы и тайминги модуля CordicParam_v2 при значении параметра PERF равным 0 и 1, при обычном конвейере итераций и при удвоенной длине конвейера (таблица 2). Понятно, что затраченные ресурсы при удвоенном конвейере будут значительно больше, но цель заключается в увеличенной максимально частоте работы алгоритма.

Таблица 2

Вариант модуля

CordicParam_v2
PERF = 0

CordicParam_v2
PERF = 1

Количество логических ячеек

1867

3326

Количество регистров

1197

3071

Максимальная частота работы

177.81 МГц

242.72 МГц

Как видим, имеет место существенное увеличение затраченных ресурсов, чуть менее чем в 2 раза, но также видим и существенное увеличение максимальной частоты работы с 177.81 до 242.72 МГц, что говорит о том, что, как говорится, игра стоит свеч.

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

Конечно, модернизировать параметризируемый rtl-код алгоритма CORDIC можно и дальше: например добавить возможность конвейезировать сумматоры каждой итерации или добавить какие-либо отладочные возможности, например assertions. Не говоря о том, что существуют множество реализаций данного алгоритма [3], различающиеся как по быстродействию, так и по используемым ресурсам (в данной статье реализован классический метод). Изначально автор хотел добавить параметр который бы позволял выбрать формат выходных данных — бинарный или комплементарный, так цифровые входные данные ЦАП могут этим отличаться, но понимая, что статья затянулась, а читатель уже и так, возможно, утомился.

Читатели могут поучаствовать в дальнейших версиях параметризируемого модуля отправив свои пожелания и идеи на daynekod85@gmail.com.

Литература

[1] Дайнеко Д. Реализация CORDIC-алгоритма на ПЛИС // Компоненты и технологии. 2011. № 12.

[2] Захаров А. В., Хачумов В. М. Алгоритмы CORDIC. Современное состояние и перспективы. М.: Физ-матлит. 2004.

[3] Uniformly Distributed CORDIC, Mario Garrido, Daniel Medina, Pedro Paz, and Marisa Lopez-Vallejo. 2025.

P.S.: Это моя первая публикация на Хабре, поэтому прошу строго не судить, хотя критика, естественно принимается). Не смог разобраться как вставить часть кода файла с указанием номеров строк, начиная с произвольного номера, а не с единицы. Может читатель подскажут как это сделать — я не смог разобраться. Также отсутствует в списке доступных языков для цветного форматирования Verilog или SystemVerilog. Присутствует только VHDL. В общем, есть некоторые неудобства, но надеюсь мне укажут на их решения).