Команде «АстроСофт» пришлось изучить возможности языка SystemVerilog, после чего нет-нет, а возникают жаркие споры о том, какая его часть синтезируема, а какая — нет. Чтобы положить конец домыслам, один из наших экспертов провёл проверку на практике. Во время разработки тестового проекта ряд вопросов удалось снять с помощью литературы, но был обнаружен один интересный момент, явного описания которого не нашлось. Чтобы исправить положение, наш коллега решил его задокументировать.
Полная версия статьи — ниже, так же она опубликована на Хабре.
Имеем проект, максимально напичканный всяческими SytemVerilog-овскими штучками. Даже если кажется, что применение той или иной вещи не даёт особого выигрыша — это ошибочное впечатление, ведь главная задача «проекта» — именно изучить возможности SystemVerilog. И вот, у нас есть набор из нескольких модулей (конкретно у меня — это UART-приёмники), данные из которых следует «сливать» в единую шину, перебирая их по алгоритму RoundRobin (конкретно в случае с UART — сливаем накопленные данные в единую очередь, которая с другой стороны будет уходить в шину USB).
module UARTreceiver(
RxBus.Slave Bus,
input logic [15:0] divider,
input logic RxD
);
// Интерфейс порта 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);
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,
// Тут мы будем перебирать элементы
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
С одной стороны, есть какая-то странная группа (я обвёл её красным), которая ни к селу, ни к городу. Но с другой стороны — на неё ничего не назначено. А наша многомерная группа — тоже имеется. И на неё можно назначать ножки. И осциллограф показывает, что всё работает верно.
Кстати, картинка RTL View стала просто прекрасной! Хоть в учебник по схемотехнике вставляй!
Замечательные возможности, предоставляемые языком SystemVerilog, прекрасно синтезируются в среде разработки Quartus II (я специально скачивал самую свежую версию, так как язык молодой, и в старых версиях Квартуса всё может быть не так радужно). К сожалению, язык обладает некоторыми неудобствами, из-за которых программирование исключительно в объектно-ориентированном мире невозможно. Но это — особенности языка. Они решаются созданием обычных сущностей, которые добавляют нагромождения в текст, но никак не влияют на сложность результирующего кода, так как являются всего лишь псевдонимами сущностей объектных.
Вопросы, о которых спорили по данной тематике у нас в компании — закрыты. Возможно, что-то из сказанного будет интересно и остальным.