В силу проектной обстановки нашей команде пришлось изучить возможности языка 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 (я специально скачивал самую свежую версию, так как язык молодой, и в старых версиях Квартуса всё может быть не так радужно). К сожалению, язык обладает некоторыми неудобствами, из-за которых программирование исключительно в объектно-ориентированном мире невозможно. Но это — особенности языка. Они решаются созданием обычных сущностей, которые добавляют нагромождения в текст, но никак не влияют на сложность результирующего кода, так как являются всего лишь псевдонимами сущностей объектных.

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

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


  1. zillant
    27.12.2017 12:04

    Интересная статья. А моделирование в Active — HDL вы не проводили?


    1. EasyLy Автор
      27.12.2017 12:36
      +1

      Я моделирую в ModelSim Altera, так как она совершенно бесплатная. Ну, или в ISIM, если работа идёт с Xilinx (хоть они и не рассматривались в данной статье). ActiveHDL — на заре тысячелетия изучал, но «париться» с ломанными версиями при наличии совершенно бесплатных и встроенных в систему разработки — а смысл?

      Но в рамках рассмотренной проблематики — проверять надо, как оно синтезируется, а Gate Level моделирование — замутное. Проще было осциллографом убедиться. Тем более, что хотелось именно реальные результаты увидеть.


      1. zillant
        27.12.2017 13:01

        Ну да, действительно. У ActiveHDL есть студенческая версия, которой я и пользуюсь, так как студент, то есть, к сожалению, с практическими задачами я еще не сталкивался. Для таких новичков как я, мне кажется, что было бы полезно в решении вашей задачи увидеть самое обычное временное моделирование)


        1. EasyLy Автор
          27.12.2017 17:30

          Кажется, я понял, где мы друг друга не понимаем. Давным-давно я прослушал курсы по программированию ПЛИС на VHDL, где нас как раз на ActiveHDL обучали. Изучили мы язык, набрались опыта, наделали лабораторных работ… И вот добыл я свою первую ПЛИС (тогда их было добыть достаточно сложно, из Швеции привёз), начинаю делать под неё первую прошивку… И на меня сыплется гора ошибок. Гружу в систему моделирования — всё прекрасно работает.

          Оказалось, что нас забыли предупредить, что есть язык, а есть — его синтезируемое подмножество. И только жалкая часть языковых конструкций будет синтезирована. Да и там — тоже свои заморочки (например, управлять сигналом можно только из одного процесса). Пришлось потратить месяц, чтобы изменить стиль разработки.

          Большинство обзорных статей про SystemVerilog едины по стилю. Сначала идёт рассказ о новых вещах, как они прекрасны. Затем — комментарий: «А это вообще синтезируемо?». И дальше — обычно разговор сходит на общие темы. Вот я и решил выбрать красивых вещей и проверить:

          1) Насколько они синтезируемы
          2) Насколько оптимален результат синтеза конкретно массивов с индексацией «на лету»

          Результаты — в статье. И именно поэтому моделирование самого языка для данной статьи бесполезно. Оно сработает, куда ж оно денется? Моделировать пришлось бы на уровне вентилей после упаковки в ПЛИС. Осциллограмма — это практически то же самое. Но глядя на результаты моделирования, скептики у нас сказали бы, что этому верить можно, но с натягом, а вот то, что видно на осциллографе — оно видно на осциллографе. Это уже физическая вещь.

          Посему в данной конкретной статье осциллограммы — более приемлемы.


        1. EasyLy Автор
          27.12.2017 18:29

          Беру свои слова обратно. Не отмоделируется… Чтобы отмоделировалось (я играл с последним примером) надо добавить начальную инициализацию переменной iter. В ПЛИС же она чему-то, да равна, а длина перебора — 4 такта. В модели же к неизвестно чему прибавляем единицу — будет опять неизвестно что. Так что

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

          Тогда вот такая моделька
          `timescale 1 ps / 1 ps
          module checkObj2();
          logic			clk50;
          logic	[1:0] button;
          logic [1:0] group [4];
          
          ObjTest2 dut(
          .clk50,
          .button,
          .group
          );
          always 
          begin
          	clk50 = 0;
          	#10000;
          	clk50 = 1;
          	#10000;
          end
          initial
          begin
          		button = 2'b00;
          		#5500000;
          		button = 1'b01;
          		#5000000;
          		button = 2'b11;
          		#5000000;
          		button = 2'b00;
          end
          endmodule
          

          даст такую картинку

          image
          Ну, а там видно, что изменение состояния кнопки (я выделил их жёлтым) даст «разбегающееся» изменение состояния ножек. С какой ножки начнётся разбег — зависит от того, в какой момент кнопку нажали (на то он и Round Robin)


    1. iBuilder
      27.12.2017 14:05

      Я пару лет назад пробовал. Active-HDL сам по себе понравился, он жутко удобен именно как среда, в которой всё есть — полное УДОБНОЕ IDE. Mоdelsim — по сути только симулятор, тот-же редактор нужно отдельно прикручивать. Но Active-HDL отставал от Mоdelsim по числу поддерживаемых фич. языка. Мне это было критично, поэтому от него отказался.
      За это время думаю много чего нагнали, но и Mоdelsim думаю не стоял на месте. Вопрос будет в том, насколько Вам будет критичны те фичи, что могут быть ещё не реализованы в Active-HDL.


  1. nerudo
    27.12.2017 13:06

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


  1. ilynxy
    27.12.2017 22:42

    Ой. А в quartus завезли modport?! Урра, это ж приятно! А он чекает направления?


    1. EasyLy Автор
      28.12.2017 05:31

      Чуть переделал наброски неопубликованного примера. Получилось примерно так:

      interface demoInterface ();
      logic		in;
      logic		out;
      modport InDevice (input in,output out);
      endinterface
      
      module ObjTest3 #(parameter cnt=4)
      (
      	input clk,
      	input button
      );
      
      demoInterface interfaces  [cnt]();
      
      fakedevice f (interfaces[0]);
      
      endmodule
      
      module fakedevice (demoInterface.InDevice Bus);
      	assign Bus.in = Bus.out;
      endmodule
      

      На что мне было сказано:
      Error (10231): Verilog HDL error at ObjTest3.sv(28): value cannot be assigned to input «in»

      Вывод: Чекает! Ну, и второй вывод — не зря я всё-таки вёл осмотр возможностей на самой свежей версии.


  1. Khort
    29.12.2017 15:07

    Одно замечание. Тулы синтеза умеют только импортировать SV; на выходе у них в 100% случаев получается обычный верилог-нетлист. Соответственно, это означает флатованные до 1-го разряда интерфейсы и дикие, трехэтажные названия портов и сигналов, полученные при конверсии имен. С такими названиями очень трудно писать констрейнты и дебажить нетлист, ведь постоянно придется сопоставлять, какой цепи и регистру в RTL соответствует цепь и регистр из нетлиста. Поэтому SV (и конструкции generate в обычном верилоге) не рекомендуют использовать в сложных проектах, а особенно — на верхнем уровне. SV — язык красивый и удобный, но только до этапа синтеза.