FPGA
December 2017 27

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

В силу проектной обстановки нашей команде пришлось изучить возможности языка SystemVerilog, после чего нет-нет, а возникают жаркие споры о том, какая его часть синтезируема, а какая — нет. Чтобы положить конец домыслам, я провёл небольшую проверку на практике. Во время разработки тестового проекта ряд вопросов удалось снять копаясь в литературе, но всплыл один интересный момент, явного описания которого не нашлось. Чтобы исправить положение, я решил его задокументировать.

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


Вот так выглядит объявление модуля 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):

,
Текстом
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];


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


Текстом
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



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


Текстом
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



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

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

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

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

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

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 бита на счётчик — итого меньше десяти триггеров получиться физически не могло)



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



Заливаем в кристалл, подключаемся к двум соседним ножкам, начинаем играть кнопкой. Получаем задержку на 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 и вспоминаем анекдот про смешанное чувство:



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



Заключение

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

Вопросы, о которых спорили по данной тематике у нас в компании — закрыты. Возможно, что-то из сказанного будет интересно и остальным.
+26
4.6k 42
Comments 10
Top of the day