+7 (812) 670-9095
Обратная связьEnglish
Главная → Статьи → Системное ПО → Проверка синтезируемости красивых возможностей SystemVerilog на практике
Версия для печати

Проверка синтезируемости красивых возможностей SystemVerilog на практике

27 декабря 2017

Команде «АстроСофт» пришлось изучить возможности языка SystemVerilog, после чего нет-нет, а возникают жаркие споры о том, какая его часть синтезируема, а какая — нет. Чтобы положить конец домыслам, один из наших экспертов провёл проверку на практике. Во время разработки тестового проекта ряд вопросов удалось снять с помощью литературы, но был обнаружен один интересный момент, явного описания которого не нашлось. Чтобы исправить положение, наш коллега решил его задокументировать.

Полная версия статьи — ниже, так же она опубликована на Хабре.






Имеем проект, максимально напичканный всяческими SytemVerilog-овскими штучками. Даже если кажется, что применение той или иной вещи не даёт особого выигрыша — это ошибочное впечатление, ведь главная задача «проекта» — именно изучить возможности SystemVerilog. И вот, у нас есть набор из нескольких модулей (конкретно у меня — это UART-приёмники), данные из которых следует «сливать» в единую шину, перебирая их по алгоритму RoundRobin (конкретно в случае с UART — сливаем накопленные данные в единую очередь, которая с другой стороны будет уходить в шину USB).


Проверка синтезируемости красивых возможностей SystemVerilog на практике


Вот так выглядит объявление модуля UART:

module UARTreceiver(
RxBus.Slave 		Bus,
input logic [15:0] 	divider,
input logic		RxD
);

Вот так выглядит его интерфейс, с которым я планирую работать в по алгоритму RoundRobin:

// Интерфейс порта FIFO для связи с группой приёмников
interface RxFifoBus #(parameter width=8)(input clk);
logic [width-1:0] data;
logic 		  rdReq;
logic 		  empty;
modport slave (input clk, rdReq, output data,empty);
modport master (input clk, data, empty, output rdReq);
endinterface

В объединяющем модуле интерфейсы описаны в виде массива, чтобы их можно было бы индексировать:

RxBus  rcvBuses [0:UARTS](.clk (Bus.clk),.reset_n);


С самими модулями они связаны через Generate:

genvar i;
generate
	for (i=0;i<UARTS;i++)
	begin : RxGen
		UARTreceiver rec (
			.Bus(rcvBuses[i]),
			.divider (16'd3125),
			.RxD (RxDs[i])
		);
	end
endgenerate

Казалось бы — красота! Знай себе занимайся коммутацией, вроде этой (я приведу пример только для линии data):


EasyLyсегодня в 11:49Проверка синтезируемости красивых возможностей SystemVerilog на практике

logic [$clog2(UARTS)-1:0] cnt;

always @ (posedge Bus.clk, negedge reset_n)
begin
	if (!reset_n) begin
		cnt <= 0;
	end else begin
		cnt <= cnt + 4'h1;
		dataToFifo [7:0] <= rcvBuses[cnt].data;
		dataToFifo [11:8] <= cnt;
	end
end

Но то — ожидание. А в реальности — получаем ошибку о невозможности доступа к объекту rcvBuses. Если индексом служит константа или genvar-переменная (собственно, тоже эквивалент константы) — без проблем, индексируйся сколько хочешь. Например, никто не запрещает сделать «в лоб»:


always_comb begin
case (cnt)
4'h0: dataToFifo [7:0] = rcvBuses[0].data;
4'h1: dataToFifo [7:0] = rcvBuses[1].data;
4'h2: dataToFifo [7:0] = rcvBuses[2].data;
4'h3: dataToFifo [7:0] = rcvBuses[3].data;
4'h4: dataToFifo [7:0] = rcvBuses[4].data;
4'h5: dataToFifo [7:0] = rcvBuses[5].data;
4'h6: dataToFifo [7:0] = rcvBuses[6].data;
4'h7: dataToFifo [7:0] = rcvBuses[7].data;
4'h8: dataToFifo [7:0] = rcvBuses[8].data;
4'h9: dataToFifo [7:0] = rcvBuses[9].data;
4'ha: dataToFifo [7:0] = rcvBuses[10].data;
4'hb: dataToFifo [7:0] = rcvBuses[11].data;
4'hc: dataToFifo [7:0] = rcvBuses[12].data;
4'hd: dataToFifo [7:0] = rcvBuses[13].data;
4'he: dataToFifo [7:0] = rcvBuses[14].data;
4'hf: dataToFifo [7:0] = rcvBuses[15].data;
default:dataToFifo [7:0] = rcvBuses[0].data;
endcase
end


Так работать будет, но теряется возможность изменять количество обрабатываемых приёмников через параметризацию модуля. А сейчас мы именно проверяем, насколько красивыми могут быть решения на исследуемом языке.

Зарывшись в литературу, я прояснил для себя, что интерфейс — вещь неупакованная. И, в отличие от структуры, он не может быть объявлен, как упакованная сущность. В знаменитой книге SystemVerilog for Design 2nd Edition в одном из примеров вскользь упомянуто (но не описано детально) решение. Необходимо выйти из красивого объектно-ориентированного мира в жестокий обычный мир, для чего добавить массив цепей:



logic [7:0] dataBuses [0:UARTS-1];


Для связи двух миров (объектного и старого) добавим такую строчку:

Проверка синтезируемости красивых возможностей SystemVerilog на практике


genvar i;
generate
	for (i=0;i<UARTS;i++)
	begin : RxGen
		assign dataBuses [i] = rcvBuses[i].data;
		UARTreceiver rec (
			.Bus(rcvBuses[i]),
			.divider (16'd3125),
			.RxD (RxDs[i])
		);
	end
endgenerate


И в цикле делаем так:

Проверка синтезируемости красивых возможностей SystemVerilog на практике


logic [$clog2(UARTS)-1:0] cnt;

always @ (posedge Bus.clk, negedge reset_n)
begin
	if (!reset_n) begin
		cnt <= 0;
	end else begin
		cnt <= cnt + 4'h1;
		dataToFifo [7:0] <= dataBuses[cnt];		
		dataToFifo [11:8] <= cnt;
	end
end


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

Хорошо. Чего можно, а чего нельзя — выяснили. Теперь было бы хорошо на простых примерах убедиться, что всё это безобразие будет синтезировано верно. Так получилось, что у меня под рукой сейчас есть только макетная плата с парой кнопок и двухканальный осциллограф. Не густо, но что есть. Попробуем придумать задачу, которая красиво докажет работоспособность (или неработоспособность) описанной выше индексации в таких спартанских условиях.

Кнопок всего две. То есть, много источников не сымитировать. Но никто же не мешает проверять всё на обратной системе. Не много шин в одну, а одну во много! Две кнопки — двухбитная шина. Будем раздавать её на ножки ПЛИС:


Проверка синтезируемости красивых возможностей SystemVerilog на практике


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

Делаем такой проект:


module ObjTest1 #(parameter cnt=4)
(
input	logic					clk50,
input logic		[1:0]		button,
output logic [cnt-1:0] 		group1,
output logic [cnt-1:0] 		group2
);


// Это чтобы осциллограф не насиловать,
// я там частоту до 1 МГц понижаю.
logic 		clk;
MainPll pll (
	.inclk0 (clk50),
	.c0 (clk)
);

// Массив двухбитных шин, которые мы будем поочерёдно
// подключать к выходной шине (с защёлкиванием)
logic	[1:0] wires [0:cnt-1];

// Связываем массив шин с обычными выходами микросхемы
// В реальной жизни, здесь мы свяжем интерфейсы блоков с 
// массивами
	genvar i;
	generate
		for (i=0;i<cnt;i++)
		begin : generilka
			assign group1 [i] = wires [i][0];
			assign group2 [i] = wires [i][1];
		end
	endgenerate

// Тут мы будем перебирать элементы
logic	[$clog2(cnt)-1:0] iter;

// Имитация подключения блока к шине
// Здесь мы просто подключаем кнопку
always_ff @(posedge clk)
begin
	iter <= iter + 1'b1;
	wires [iter][0] <= button[0];
	wires [iter][1] <= button[1];
end

endmodule


Из того, что я пока не описывал — PLL. В одной из прошлых статей я пришёл к ошибочным выводам, работая на высоких пределах осциллографа. Чтобы исключить подобное, PLL снижает частоту до одного мегагерца. Остальное — уже описывалось. Поэтому пробежимся по самым вершкам:

Фактические ножки микросхемы описываются в виде двух векторов. Не очень красиво, но потом украсим:


output logic [cnt-1:0] 		group1,
output logic [cnt-1:0] 		group2


А пока — связываем их с исследуемым массивом:

// Массив двухбитных шин, которые мы будем поочерёдно
// подключать к выходной шине (с защёлкиванием)
logic	[1:0] wires [0:cnt-1];


вот таким образом:

// Связываем массив шин с обычными выходами микросхемы
// В реальной жизни, здесь мы свяжем интерфейсы блоков с 
// массивами
	genvar i;
	generate
		for (i=0;i<cnt;i++)
		begin : generilka
			assign group1 [i] = wires [i][0];
			assign group2 [i] = wires [i][1];
		end
	endgenerate


Кнопки — описываются в виде шины:

input logic		[1:0]		button,


И алгоритм Round Robin реализуем следующим образом:

// Тут мы будем перебирать элементы
logic	[$clog2(cnt)-1:0] iter;

// Имитация подключения блока к шине
// Здесь мы просто подключаем кнопку
always_ff @(posedge clk)
begin
	iter <= iter + 1'b1;
	wires [iter] <= button;
end


Компилируем, наслаждаемся тем, сколько ресурсов всё это заняло (у нас защёлкивается 8 ножек, плюс 2 бита на счётчик — итого меньше десяти триггеров получиться физически не могло)


Проверка синтезируемости красивых возможностей SystemVerilog на практике

RTL Viewer также не показывает ничего лишнего. Есть PLL, есть счётчик, есть дешифратор, есть триггеры, объединённые в двухбитные шины. Всё, как мы просили:


Проверка синтезируемости красивых возможностей SystemVerilog на практике


Заливаем в кристалл, подключаемся к двум соседним ножкам, начинаем играть кнопкой. Получаем задержку на 1 микросекунду, что соответствует частоте 1 МГц.


Получаем задержку на 1 микросекунду, что соответствует частоте 1 МГц.

Переносим второй щуп на следующую ножку:

Переносим второй щуп на следующую ножку:

И на следующую:

И на следующую:


Всё соответствует теории. На другую кнопку эта половина не реагирует.

Ну и, наконец, проверим, что нам скажет среда разработки, если мы опишем ножки не как две группы контактов, а как единый массив, что позволит избежать занудного блока generate, связывающего массив с группами. Такой код не содержит совсем ничего лишнего, только суть исследования (ну, и PLL, переносящий результаты в хорошо различимую на осциллографе область):



module ObjTest2 #(parameter cnt=4)
(
input	logic					clk50,
input logic	 [1:0]		button,
output logic [1:0] 		group [0:cnt-1]
);


// Это чтобы осциллограф не насиловать,
// я там частоту до 1 МГц понижаю.
logic 		clk;
MainPll pll (
	.inclk0 (clk50),
	.c0 (clk)
);

// Тут мы будем перебирать элементы
logic	[$clog2(cnt)-1:0] iter;

// Имитация подключения блока к шине
// Здесь мы просто подключаем кнопку
always_ff @(posedge clk)
begin
	iter <= iter + 1'b1;
	group [iter] <= button;
end

endmodule


Идём в Pin Planner и вспоминаем анекдот про смешанное чувство:

Pin Planner



С одной стороны, есть какая-то странная группа (я обвёл её красным), которая ни к селу, ни к городу. Но с другой стороны — на неё ничего не назначено. А наша многомерная группа — тоже имеется. И на неё можно назначать ножки. И осциллограф показывает, что всё работает верно.

Кстати, картинка RTL View стала просто прекрасной! Хоть в учебник по схемотехнике вставляй!



Проверка синтезируемости красивых возможностей SystemVerilog на практике


Заключение

Замечательные возможности, предоставляемые языком SystemVerilog, прекрасно синтезируются в среде разработки Quartus II (я специально скачивал самую свежую версию, так как язык молодой, и в старых версиях Квартуса всё может быть не так радужно). К сожалению, язык обладает некоторыми неудобствами, из-за которых программирование исключительно в объектно-ориентированном мире невозможно. Но это — особенности языка. Они решаются созданием обычных сущностей, которые добавляют нагромождения в текст, но никак не влияют на сложность результирующего кода, так как являются всего лишь псевдонимами сущностей объектных.

Вопросы, о которых спорили по данной тематике у нас в компании — закрыты. Возможно, что-то из сказанного будет интересно и остальным.


Теги: systemverilog, ПЛИС, fpga