
Введение
Описание модуля с фиксированными параметрами — CordicSimple_v1
Небольшая модернизация — CordicSimple_v2
Описание параметризируемого модуля — CordicParam_v1
Модернизация: возможность увеличения производительности — CordicParam_v2
Итоги
1. Введение
В далеком 2011 году автором была опубликована статья «Реализация CORDIC‑алгоритма на ПЛИС» [1]. В той статье приводится сначала математическое описание алгоритма, его суть. Показан пример расчета поворота вектора на плоскости сначала «на бумажке» согласно алгоритму, а затем сравнение результатов с расчетом «по калькулятору». Затем, показано создание структурной схемы проекта с rtl‑описанием CORDIC‑алгоритма и приведены листинги каждого модуля. Помимо этого были приведены основы создания проекта в среде ModelSim.
Автор считает, что эта статья оказалась полезной для новичков в области программирования ПЛИС, так как на протяжении долгого периода времени, после публикации статьи приходили письма с вопросами и уточнениями на данную тему. Даже сейчас я часто встречаю на различных форумах на тему ПЛИС ссылки на данную работу. Но! Если математическая часть алгоритма однозначно является полезной, а также основы создания и структурированного ведения проекта для новичка, то использовать данное rtl‑описание в различных проектах, адаптируя модули проекта по свои нужды крайне неудобно и неуклюже. По крайней мере, автор, когда заглядывает в это свое создание в прошлом, морщится и чувствует себя неловко.
Поэтому появилось желание представить тот модуль CORDICа (для генерации гармонического сигнала в сфере радиолокации и связи), который автор использует в данный момент в различных проектах. Весь алгоритм реализован в одном sv‑модуле, а, значит, его намного легче переносить из проекта в проект. Также он более прост в восприятии.
После этого автор попробует его максимально параметризировать.
Считаю нужным повторить очень кратко математическую часть алгоритма CORDIC. Данный алгоритм позволяет простейшими операциями реализовать поворот вектора на плоскости (Рис.1):
Короче говоря, зная координаты (X0, Y0) исходного положения вектора, если стоит задача повернуть вектор на угол Z, то координаты нового положения вектора (X1, Y1), можно вычислить через следующие тригонометрические формулы (Рис.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 поворота вектора на некоторый угол могут выглядеть следующим образом (Рис.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). Звучит непонятно, иллюстрация ниже поможет восприятию, так как гармонический сигнал можно назвать движением точки по кругу, расстянутом во времени.»
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). Исходный код и тестбенч для моделирования приведены по ссылке.
Здесь, в качестве примера, был сгенерирован квадратурный сигнал частотой 2 МГц. Тактовая частота clk = 100 МГц. Шаг аккумулятора фазы acc = 2 / 100 * 2^32 = 0x051EB852.
Не являясь мастером по MatLAB, все-таки интересно было написать простенький скрипт, вычисляющий спектр сгенерированного сигнал с помощью БПФ с количеством точек 4096 (риc. 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):

Как видим, результат практически идентичен, максимальное отклонение, которое автор визуально заметил, составляет 2 бита. Вероятно это связано именно с приведением поворотных коэффициентов как 32-х разрядному числу. Повторюсь, в конце статьи, в эпилоге этот момент будет упомянут.
Теперь, хотелось бы сравнить спектр сгенерированного гармонического сигнала, показанного на рисунке … со спектром гармонического сигнала, полученного с помощью параметризируемого модуля CordicParam_v1 (рис.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):
Как видно, между «грядкой» сумматоров и мультиплексоров можно расположить еще регистры. Таким образом можно уменьшить комбинационную цепочку между регистрами, но получим примерное удвоение количество используемых логических элементов, а также временнОе увеличение конвейера также в два раза.
В шапке модуля 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):

Как видно, между регистрами конвейера, например 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.

Код модуля CordicParam_v2 доступен по ссылке.
6. Итоги
Осталось сравнить затраченные ресурсы и тайминги модуля CordicParam_v2 при значении параметра PERF равным 0 и 1, при обычном конвейере итераций и при удвоенной длине конвейера (таблица 2). Понятно, что затраченные ресурсы при удвоенном конвейере будут значительно больше, но цель заключается в увеличенной максимально частоте работы алгоритма.
Таблица 2
Вариант модуля | CordicParam_v2 | CordicParam_v2 |
Количество логических ячеек | 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. В общем, есть некоторые неудобства, но надеюсь мне укажут на их решения).
